From 0ef9d29bfef6b0c9773e8f3cb1e939d8f2424aa9 Mon Sep 17 00:00:00 2001 From: lance Date: Sun, 10 Mar 2019 21:39:34 -0700 Subject: [PATCH 1/3] 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 From bbe284e280869c822f8aac487cafe40c4a575752 Mon Sep 17 00:00:00 2001 From: lance Date: Sun, 10 Mar 2019 21:40:14 -0700 Subject: [PATCH 2/3] removing todo item from readme --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 4839c55..70b0295 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,3 @@ $ zappa invoke secretshare.cleanup.purge_expired_secrets Most people don't necessarily care about the backend API - this is only one half of the battle as we need something to present the information. This API can be used in conjunction with any static website with simple Javascript for posting, retrieving, and rendering the data. Here's an example one: [secretshare-static](https://github.com/lalanza808/secretshare-static) - -## To-do - -* Add expiration after first read payload option From d14c024d9c1999f1489d5b84fb60d6e10be13dd2 Mon Sep 17 00:00:00 2001 From: lance Date: Sun, 10 Mar 2019 21:52:00 -0700 Subject: [PATCH 3/3] minor edits --- secretshare/app.py | 3 --- secretshare/library/secretsmanager.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/secretshare/app.py b/secretshare/app.py index 1c9b58d..6295c1e 100644 --- a/secretshare/app.py +++ b/secretshare/app.py @@ -52,16 +52,13 @@ class Secrets(Resource): secret = secretsmanager.Secret(secret_name=secret_name) if secret.exists and not secret.expired: - # If secret exists and not expired, return secret return secret.retrieve(), 200 else: - # If secret is expired or doesn't exist, return error return { 'error_msg': 'This secret is expired or does not exist.', 'error_id': 'expired_secret' }, 400 else: - # If no query string provided, return error return { 'error_msg': 'No secret token provided.', 'error_id': 'no_token' diff --git a/secretshare/library/secretsmanager.py b/secretshare/library/secretsmanager.py index cf3948e..d503845 100644 --- a/secretshare/library/secretsmanager.py +++ b/secretshare/library/secretsmanager.py @@ -90,7 +90,7 @@ class Secret(object): if not expiration: # Set default time expiration - self.expiration = str(now.shift(hours=app.config['DEFAULT_HOURS'])) + self.expiration = str(now.shift(hours=app.config.get('DEFAULT_HOURS'))) else: # Otherwise validate we have a good expiration date try: