From 0ef9d29bfef6b0c9773e8f3cb1e939d8f2424aa9 Mon Sep 17 00:00:00 2001 From: lance Date: Sun, 10 Mar 2019 21:39:34 -0700 Subject: [PATCH] updating app to accept an "expire_on_read" header to expire the record once it's been read --- secretshare/app.py | 8 +++- secretshare/cleanup.py | 38 ++++++++++++++--- secretshare/library/secretsmanager.py | 59 +++++++++++---------------- 3 files changed, 62 insertions(+), 43 deletions(-) diff --git a/secretshare/app.py b/secretshare/app.py index 7d0e347..1c9b58d 100644 --- a/secretshare/app.py +++ b/secretshare/app.py @@ -22,6 +22,7 @@ secret_data = api.model('secret_data', { 'password': fields.String, 'message': fields.String, 'expiration': fields.DateTime, + 'expire_on_read': fields.Boolean }) response_data = api.inherit('response_data', secret_data, { 'token': fields.String, @@ -77,9 +78,12 @@ class Secrets(Resource): username=api.payload.get('username', ''), password=api.payload.get('password', ''), message=api.payload.get('message', ''), - expiration=api.payload.get('expiration', '') + expiration=api.payload.get('expiration', ''), + expire_on_read=api.payload.get('expire_on_read', False) ) - return {'token': secret.secret_name}, 201 + return { + 'token': secret.secret_name + }, 201 except ValueError as err: return { 'error_msg': 'Invalid expiration date', diff --git a/secretshare/cleanup.py b/secretshare/cleanup.py index dca9cc4..3fd232f 100644 --- a/secretshare/cleanup.py +++ b/secretshare/cleanup.py @@ -7,15 +7,43 @@ from arrow import utcnow as arrow_utcnow from secretshare.library import secretsmanager +def list_secrets(boto_client): + """Return a list of all secrets""" + next_token = "" + pagination_finished = False + secrets = [] + response = boto_client.list_secrets( + MaxResults=20 + ) + while not pagination_finished: + for secret in response['SecretList']: + secrets.append(secret) + if 'NextToken' in response: + next_token = response['NextToken'] + response = boto_client.list_secrets( + MaxResults=20, + NextToken=next_token + ) + else: + pagination_finished = True + + return secrets + +def delete_secret(boto_client, secret_name): + """Remove a secret""" + response = boto_client.delete_secret( + SecretId=secret_name, + ForceDeleteWithoutRecovery=True + ) + return response + def purge_expired_secrets(): - """Purge all expired secrets. This is run as - a recurring Lambda function. - """ + """Purge all expired secrets.""" client = boto3_client('secretsmanager') - all_secrets = secretsmanager.list_secrets(client) + all_secrets = list_secrets(client) for secret_data in all_secrets: secret = secretsmanager.Secret() secret.check_tags_expired(secret_data) if secret.expired: print(f"[+] Purging expired secret {secret_data['Name']}") - secretsmanager.delete_secret(client, secret_data['Name']) + delete_secret(client, secret_data['Name']) diff --git a/secretshare/library/secretsmanager.py b/secretshare/library/secretsmanager.py index ec262fb..cf3948e 100644 --- a/secretshare/library/secretsmanager.py +++ b/secretshare/library/secretsmanager.py @@ -4,6 +4,7 @@ from boto3 import client as boto3_client from json import loads as json_loads from json import dumps as json_dumps +from ast import literal_eval from arrow import get as arrow_get from arrow import utcnow as arrow_utcnow from flask import current_app as app @@ -20,6 +21,7 @@ class Secret(object): def __init__(self, secret_name=''): self.secret_name = secret_name + # Perform a check if secret name provided if self.secret_name: self.check() @@ -40,12 +42,14 @@ class Secret(object): self.expired = None def check_tags_expired(self, json_data): - """Given a DescribeSecret JSON response and assess whether - the 'Expiration' tag shows the secret is expired or not + """Given a DescribeSecret JSON response, assess whether + the metadata tags show the secret is expired or not """ now = arrow_utcnow() for tag in json_data['Tags']: + + # Set expiration equal to tag value - perform date delta vs now if 'Expiration' in tag.values(): self.expiration = tag['Value'] expiration_date = arrow_get(tag['Value']) @@ -55,10 +59,20 @@ class Secret(object): else: self.expired = False + # Set value if it's set to expire after first read + if 'ExpireOnRead' in tag.values(): + self.expire_on_read = literal_eval(str(tag['Value']).title()) + else: + self.expire_on_read = False + + # If 'LastAccessedDate' in json then the record has been read + if 'LastAccessedDate' in json_data and self.expire_on_read: + self.expired = True + return - def create(self, username, password, message, expiration=''): + def create(self, username, password, message, expiration='', expire_on_read=False): """Create a secret""" now = arrow_utcnow() @@ -67,6 +81,7 @@ class Secret(object): self.username = str(username) self.password = str(password) self.message = str(message) + self.expire_on_read = bool(expire_on_read) data_object = { "username": self.username, "password": self.password, @@ -98,6 +113,10 @@ class Secret(object): 'Key': 'Expiration', 'Value': self.expiration }, + { + 'Key': 'ExpireOnRead', + 'Value': str(self.expire_on_read) + } ] ) @@ -113,38 +132,6 @@ class Secret(object): ) secret_value = json_loads(response["SecretString"]) secret_value['expiration'] = self.expiration + secret_value['expire_on_read'] = self.expire_on_read return secret_value - - -# One-off functions used by cleanup Lambda - -def list_secrets(boto_client): - """List all secrets""" - next_token = "" - pagination_finished = False - secrets = [] - response = boto_client.list_secrets( - MaxResults=20 - ) - while not pagination_finished: - for secret in response['SecretList']: - secrets.append(secret) - if 'NextToken' in response: - next_token = response['NextToken'] - response = boto_client.list_secrets( - MaxResults=20, - NextToken=next_token - ) - else: - pagination_finished = True - - return secrets - -def delete_secret(boto_client, secret_name): - """Remove a secret""" - response = boto_client.delete_secret( - SecretId=secret_name, - ForceDeleteWithoutRecovery=True - ) - return response