Skip to content

Commit

Permalink
move functions to libraries
Browse files Browse the repository at this point in the history
  • Loading branch information
Brad Duncan committed Feb 26, 2024
1 parent 271b97d commit 15f1cc1
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 0 deletions.
27 changes: 27 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Build

on:
push:
branches:
- main


jobs:
build:
name: Build
runs-on: ubuntu-latest
permissions: read-all
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- uses: sonarsource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
# If you wish to fail your job when the Quality Gate is red, uncomment the
# following lines. This would typically be used to fail a deployment.
# - uses: sonarsource/sonarqube-quality-gate-action@master
# timeout-minutes: 5
# env:
# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
venv/
.vscode/
__pycache__/
10 changes: 10 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import os

aws_region = os.getenv('AWS_REGION', '')
cognito_user_pool_id = os.getenv('COGNITO_USER_POOL_ID', '')
s3_bucket_name = os.getenv('S3_BUCKET_NAME', '')
s3_key = os.getenv('S3_KEY', '')
sns_topic_arn = os.getenv('SNS_TOPIC_ARN', '')
aged_user_threshold_minutes = os.getenv('AGED_USER_THRESHOLD_MINUTES', '1')
user_status = os.getenv('USER_STATUS', 'UNCONFIRMED,RESET_REQUIRED,FORCE_CHANGE_PASSWORD')
delete_enabled = os.getenv('DELETE_ENABLED', 'False')
50 changes: 50 additions & 0 deletions lambda_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import boto3
from library import *
from config import *
from datetime import datetime
import pytz

# Initialize the Boto3 clients and environment variables
cognito_client = boto3.client('cognito-idp', region_name=aws_region)
s3_client = boto3.client('s3', region_name=aws_region)
sns_client = boto3.client('sns', region_name=aws_region)

# Caching the last processed time and deleted users
last_processed_time_cache = {}
deleted_users_cache = set()

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

# List all unconfirmed users, considering the last processed time from the cache
unconfirmed_users = list_users(cognito_client, cognito_user_pool_id, aged_user_threshold_minutes, user_status, last_processed_time_cache)

# Print the list of unconfirmed users
print(f"{user_status} users older than {aged_user_threshold_minutes} minutes: {unconfirmed_users}")

# Delete the unconfirmed users and collect the ones successfully deleted
deleted_users = [user for user in unconfirmed_users if delete_users(cognito_client, 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, s3_bucket_name, s3_key)
# Send an email notification with the list of deleted users
send_email_notification(sns_client, sns_topic_arn, deleted_users)
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
last_processed_time_cache[cognito_user_pool_id] = current_run_time
else:
message = "No unconfirmed users to delete or delete not enabled."

return {
'statusCode': 200,
'body': message
}

if __name__ == '__main__':
event = {}
context = {}
main_handler(event, context)

3 changes: 3 additions & 0 deletions library/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .cognito_manager import *
from .file_manager import *
from .notification_service import *
88 changes: 88 additions & 0 deletions library/cognito_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from datetime import datetime, timedelta
import pytz

def list_users(cognito_client, user_pool_id, age_in_minutes, user_statuses, last_run_cache):
"""
List users from Cognito based on specified statuses and age, adjusted by the last processed time in cache.
:param cognito_client: The Boto3 client for Cognito
:param user_pool_id: The ID of the Cognito User Pool
: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
"""
try:
# Ensure age_in_minutes is an integer
age_in_minutes = int(age_in_minutes)

# Attempt to get the last run time from cache
last_run_time = last_run_cache.get(user_pool_id)

# Calculate the cutoff time
if last_run_time:
cutoff_time = last_run_time
else:
# 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 = []

# Split the user_statuses into a list
statuses = user_statuses.split(',')

# Paginate through the list_users response for each status
for status in statuses:
paginator = cognito_client.get_paginator('list_users')
for page in paginator.paginate(UserPoolId=user_pool_id, Filter=f'cognito:user_status="{status}"'):
for user in page['Users']:
user_creation_time = user['UserCreateDate']

# Ensure user_creation_time is timezone-aware and in UTC
if user_creation_time.tzinfo is None or user_creation_time.tzinfo.utcoffset(user_creation_time) is None:
user_creation_time = pytz.utc.localize(user_creation_time)

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

return aged_users
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):
"""
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 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
"""
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

cognito_client.admin_delete_user(
UserPoolId=user_pool_id,
Username=username
)

# Update cache after successful deletion
deleted_users_cache.add(username)
return True
except Exception as e:
print(f"Error deleting user {username}: {e}")
return False
else:
print(f"Skipping deletion of user {username} as delete_enabled is not set to 'True'")
return False
24 changes: 24 additions & 0 deletions library/file_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
def write_deleted_users_to_s3(s3_client, deleted_users, bucket_name=None, s3_key=None):
"""
Writes the list of deleted users to a file in an S3 bucket. 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 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(f"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)

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)
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}")

30 changes: 30 additions & 0 deletions library/notification_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
def send_email_notification(sns_client, topic_arn, deleted_users):
"""
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
"""
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:
print("No users deleted, skipping SNS notification...")
return

# Creating the message
message = "Deleted Users:\n" + "\n".join(deleted_users)

# Sending the message
try:
response = sns_client.publish(
TopicArn=topic_arn,
Message=message,
Subject='Notification of Deleted Users'
)
print(f"Message sent to SNS topic {topic_arn}. Message ID: {response['MessageId']}")
except Exception as e:
print(f"Failed to send notification due to an error: {e}")
1 change: 1 addition & 0 deletions sonar-project.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sonar.projectKey=XargsUK_cognito-cleaner_AY3lY9z82k2cii8YoWbp

0 comments on commit 15f1cc1

Please sign in to comment.