From f9fe342db82fbb8e741b9a85f9dde266ca6abedc Mon Sep 17 00:00:00 2001 From: Scott Pickett Date: Mon, 12 Feb 2024 14:23:18 -0700 Subject: [PATCH] add api key feature to application --- .gitignore | 2 ++ flask/app/app.py | 53 +++++++++++++++++++++++++++- flask/app/credentials/api_keys.jsonk | 10 ++++++ flask/readme.md | 51 +++++++++++++++++++++++--- readme.md | 6 ++++ 5 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 flask/app/credentials/api_keys.jsonk diff --git a/.gitignore b/.gitignore index 1721324..0beec50 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ /flask/logs /.vscode # .env removed for docker config directives +.jsonk + diff --git a/flask/app/app.py b/flask/app/app.py index 865eb5c..d1fde66 100644 --- a/flask/app/app.py +++ b/flask/app/app.py @@ -1,9 +1,59 @@ -from flask import Flask, render_template, request, url_for +import json +from functools import wraps +from flask import request, abort, Flask, render_template, request, url_for import app.server as server app = Flask(__name__) +def require_api_key(view_function): + """ + Decorator to enforce API key validation on Flask view functions. + + This decorator loads the list of valid API keys from a specified JSON file. + + It then checks the incoming request for an 'X-Api-Key' header and validates + the provided API key against the list of keys loaded from the file. + + If the API key is valid, the request is allowed to proceed to + the decorated view function. If the API key is invalid or missing, + an HTTP 401 Unauthorized error is returned. + + Args: + view_function (function): The Flask view function to be decorated. + + Returns: + function: The decorated view function which includes API key validation. + + Raises: + FileNotFoundError: If the API keys file does not exist. + with HTTP 500 Internal Server Error + json.JSONDecodeError: If there is an error parsing the API keys file. + with HTTP 500 Internal Server Error + HTTP 401 Unauthorized if the API key is invalid or missing + """ + @wraps(view_function) + def decorated_function(*args, **kwargs): + # Load API keys + try: + with open('app/credentials/api_keys.jsonk', 'r') as file: + api_keys_data = json.load(file) + except (FileNotFoundError, json.JSONDecodeError) as e: + abort(500, f'Error loading or parsing API keys file: {e}') + + # Retrieve the API key from the request header + provided_api_key = request.headers.get('X-Api-Key') + + # Check if the provided API key matches any key in the file + for item in api_keys_data.values(): + if provided_api_key == item.get('key'): + return view_function(*args, **kwargs) + + # If no matching key is found, return an unauthorized error + abort(401) # Unauthorized access + return decorated_function + + @app.route('/', methods=['GET']) def root_get(): """The default GET view for hitting the application with a browser @@ -39,6 +89,7 @@ def api_get() -> dict: @app.route('/api', methods=['POST']) +@require_api_key def api_post() -> dict: """The POST view for the API. Submitted data is passed to the "back-end" and "reversed". Returned data is given to the rendered template. diff --git a/flask/app/credentials/api_keys.jsonk b/flask/app/credentials/api_keys.jsonk new file mode 100644 index 0000000..9214788 --- /dev/null +++ b/flask/app/credentials/api_keys.jsonk @@ -0,0 +1,10 @@ +{ + "0": { + "key": "MyAPIKey1", + "comment": "This key is for Application A" + }, + "1": { + "key": "YourActualAPIKeyHere", + "comment": "This key is for Application B" + } +} diff --git a/flask/readme.md b/flask/readme.md index 53c3e50..6b2d653 100644 --- a/flask/readme.md +++ b/flask/readme.md @@ -20,9 +20,10 @@ flask/ app.py <--- the main applicaiton file with the Flask views ("front-end") server.py <--- the "back-end" application file with application functions wsgi.py <--- the web server callable WSGI Python file to launch the Flask app - |---static/ <--- directory for static front-end resources, like CSS files - |---templates/ <--- directory for HTML code - |---work_files/ <--- optional directory for writing out or reading in data + |---static/ <--- directory for static front-end resources, like CSS files + |---templates/ <--- directory for HTML code + |---work_files/ <--- optional directory for writing out or reading in data + |---credentials/ <--- credentials for accessing the app, when required |---tests/ <--- directory containing all Python tests (see "tests" below) |---coverage <--- directory containing coverage.py resources .coveragerc <--- the configuration file for coverage.py @@ -32,6 +33,42 @@ flask/ readme.md <--- this file ``` +## API Keys File (`api_keys.jsonk`) + +### Overview + +The `api_keys.jsonk` file is used to store API keys for authenticating requests to selected endpoints of the application. Each key is associated with a comment explaining its purpose or the application it is intended for. This file should be placed in the `credentials` directory at the root of the app. + +### Format + +The file contains a JSON object where each entry consists of a numeric string as a key, and the value is another object with two properties: `key` and `comment`. The `key` property holds the actual API key, while the `comment` property provides a description or note about the key. + +### Example + +```json +{ + "0": { + "key": "MyAPIKey1", + "comment": "This key is for Application A" + }, + "1": { + "key": "AnotherAPIKey", + "comment": "This key is for Application B" + } +} +``` + +### Usage + +The application reads this file to authenticate API requests. When making a request, include the API key in the X-Api-Key request header. The server will verify the API key against those listed in the api_keys.jsonk file. + +### Security + +- DO NOT commit this file to version control. +- Ensure api_keys.jsonk is listed in your .gitignore file to prevent accidental exposure of API keys. +- Regularly rotate and review API keys to ensure they remain secure. +- Future: transition to a more secure key management system for production environments. + ## Tests ### Running unit tests @@ -60,5 +97,9 @@ HTML test coverage report: ### web test -```curl -X GET localhost:8080/api``` -```curl -X POST -d data="reverse me" server1:8080/api``` +#### GET method +``` curl -X GET http://localhost:8080/api``` + +#### POST method with API Key +```curl -X POST -H "X-Api-Key: YourActualAPIKeyHere" -d "data=reverse me" http://localhost:8080/api``` + diff --git a/readme.md b/readme.md index 8260ab5..2fb503e 100644 --- a/readme.md +++ b/readme.md @@ -32,6 +32,12 @@ sudo docker container ls sudo docker-compose up --build -d ``` +### Restart the containers + +``` +sudo docker-compose restart +``` + #### Stop containers and remove them, if needed ```