From da446aca6c8f5147f67a86d1f209c7306939cf56 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Sat, 27 Aug 2022 00:30:15 +0000 Subject: [PATCH 01/19] refactor: move db and User into database module this is to prepare for moving setup and teardown into the same module make sure tests have an app context so that the db object knows which app to work with --- eligibility_server/app.py | 15 ++--------- eligibility_server/database.py | 22 ++++++++++++--- setup.py | 49 ++++++++++++++++++---------------- teardown.py | 31 +++++++++++---------- tests/conftest.py | 2 ++ tests/test_database.py | 1 + tests/test_verify.py | 2 ++ 7 files changed, 69 insertions(+), 53 deletions(-) diff --git a/eligibility_server/app.py b/eligibility_server/app.py index cbe106d9..6813d99c 100644 --- a/eligibility_server/app.py +++ b/eligibility_server/app.py @@ -5,11 +5,11 @@ from flask import Flask, jsonify, make_response from flask_restful import Api -from flask_sqlalchemy import SQLAlchemy from flask.logging import default_handler from .verify import Verify from .keypair import get_server_public_key +from . import database app = Flask(__name__) app.config.from_object("eligibility_server.settings") @@ -67,18 +67,7 @@ def internal_server_error(error): api = Api(app) api.add_resource(Verify, "/verify") -db = SQLAlchemy(app) - - -class User(db.Model): - id = db.Column(db.Integer, primary_key=True) - sub = db.Column(db.String, unique=True, nullable=False) - name = db.Column(db.String, unique=True, nullable=False) - types = db.Column(db.String, unique=False, nullable=False) - - def __repr__(self): - return "" % self.sub - +database.init_app(app) if __name__ == "__main__": app.run(host=app.config["HOST"], debug=app.config["DEBUG_MODE"], port="8000") # nosec diff --git a/eligibility_server/database.py b/eligibility_server/database.py index e12a6952..b0f68a40 100644 --- a/eligibility_server/database.py +++ b/eligibility_server/database.py @@ -3,12 +3,28 @@ """ import ast - -from . import app import logging +from flask_sqlalchemy import SQLAlchemy + logger = logging.getLogger(__name__) +db = SQLAlchemy() + + +def init_app(app): + db.init_app(app) + + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + sub = db.Column(db.String, unique=True, nullable=False) + name = db.Column(db.String, unique=True, nullable=False) + types = db.Column(db.String, unique=False, nullable=False) + + def __repr__(self): + return "" % self.sub + class Database: def __init__(self, hash=False): @@ -44,7 +60,7 @@ def check_user(self, sub: str, name: str, types: str) -> list: sub = self._hash.hash_input(sub) name = self._hash.hash_input(name) - existing_user = app.User.query.filter_by(sub=sub, name=name).first() + existing_user = User.query.filter_by(sub=sub, name=name).first() if existing_user: existing_user_types = ast.literal_eval(existing_user.types) else: diff --git a/setup.py b/setup.py index ef20de03..db1e7853 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,11 @@ import csv import json +import logging from flask_sqlalchemy import inspect -from eligibility_server import app -import logging + +from eligibility_server.app import app +from eligibility_server.database import db, User logger = logging.getLogger("setup") @@ -18,7 +20,7 @@ def import_users(): configurations: CSV_DELIMITER, CSV_NEWLINE, CSV_QUOTING, CSV_QUOTECHAR """ - file_path = app.app.config["IMPORT_FILE_PATH"] + file_path = app.config["IMPORT_FILE_PATH"] logger.info(f"Importing users from {file_path}") file_format = file_path.split(".")[-1] @@ -29,19 +31,19 @@ def import_users(): for user in data: save_users(user, data[user][0], str(data[user][1])) elif file_format == "csv": - with open(file_path, newline=app.app.config["CSV_NEWLINE"], encoding="utf-8") as file: + with open(file_path, newline=app.config["CSV_NEWLINE"], encoding="utf-8") as file: data = csv.reader( file, - delimiter=app.app.config["CSV_DELIMITER"], - quoting=int(app.app.config["CSV_QUOTING"]), - quotechar=app.app.config["CSV_QUOTECHAR"], + delimiter=app.config["CSV_DELIMITER"], + quoting=int(app.config["CSV_QUOTING"]), + quotechar=app.config["CSV_QUOTECHAR"], ) for user in data: save_users(user[0], user[1], user[2]) else: logger.warning(f"File format is not supported: {file_format}") - logger.info(f"Users added: {app.User.query.count()}") + logger.info(f"Users added: {User.query.count()}") def save_users(sub: str, name: str, types: str): @@ -53,22 +55,23 @@ def save_users(sub: str, name: str, types: str): @param types - Types of eligibilities, in a stringified list """ - item = app.User(sub=sub, name=name, types=types) - app.db.session.add(item) - app.db.session.commit() + item = User(sub=sub, name=name, types=types) + db.session.add(item) + db.session.commit() if __name__ == "__main__": - inspector = inspect(app.db.engine) - - if inspector.get_table_names(): - logger.info("Tables already exist.") - if app.User.query.count() == 0: - import_users() + with app.app_context(): + inspector = inspect(db.engine) + + if inspector.get_table_names(): + logger.info("Tables already exist.") + if User.query.count() == 0: + import_users() + else: + logger.info("User table already has data.") else: - logger.info("User table already has data.") - else: - logger.info("Creating table...") - app.db.create_all() - logger.info("Table created.") - import_users() + logger.info("Creating table...") + db.create_all() + logger.info("Table created.") + import_users() diff --git a/teardown.py b/teardown.py index 64c08403..244c5b78 100644 --- a/teardown.py +++ b/teardown.py @@ -1,22 +1,25 @@ -from flask_sqlalchemy import inspect -from eligibility_server import app import logging +from flask_sqlalchemy import inspect + +from eligibility_server.app import app +from eligibility_server.database import db, User logger = logging.getLogger("teardown") if __name__ == "__main__": - inspector = inspect(app.db.engine) + with app.app_context(): + inspector = inspect(db.engine) - if inspector.get_table_names(): - try: - logger.info(f"Users to be deleted: {app.User.query.count()}") - app.User.query.delete() - app.db.session.commit() - except Exception as e: - logger.warning("Failed to query for Users", e) + if inspector.get_table_names(): + try: + logger.info(f"Users to be deleted: {User.query.count()}") + User.query.delete() + db.session.commit() + except Exception as e: + logger.warning("Failed to query for Users", e) - app.db.drop_all() - logger.info("Database dropped.") - else: - logger.info("Database does not exist.") + db.drop_all() + logger.info("Database dropped.") + else: + logger.info("Database does not exist.") diff --git a/tests/conftest.py b/tests/conftest.py index c451befc..5eae20b2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,8 @@ @pytest.fixture def flask(): + app.app_context().push() + yield app diff --git a/tests/test_database.py b/tests/test_database.py index be6cda3b..88516990 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -28,6 +28,7 @@ def test_database_init_default(): assert database._hash is False +@pytest.mark.usefixtures("flask") @pytest.mark.parametrize("db, sub, name, types, expected", test_data) def test_database_check_user(db, sub, name, types, expected): assert db.check_user(sub, name, types) == expected diff --git a/tests/test_verify.py b/tests/test_verify.py index 882f221a..d7964daf 100644 --- a/tests/test_verify.py +++ b/tests/test_verify.py @@ -1,4 +1,5 @@ import json +import pytest import uuid from eligibility_server.verify import Verify @@ -25,6 +26,7 @@ def test_Verify_client_get_bad_request(mocker, client): assert response.json["message"].startswith("Bad request") +@pytest.mark.usefixtures("flask") def test_Verify_get_response_sub_format_match(mocker): mocked_config = {"SUB_FORMAT_REGEX": r"^[A-Z]\d{7}$", "INPUT_HASH_ALGO": ""} mocker.patch.dict("eligibility_server.verify.app.app.config", mocked_config) From 258f95860173e41c05166ac33ac3807fe927617b Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Sat, 27 Aug 2022 01:31:11 +0000 Subject: [PATCH 02/19] refactor: move Database into db package --- eligibility_server/database.py | 53 ---------------------------- eligibility_server/db/__init__.py | 58 +++++++++++++++++++++++++++++++ eligibility_server/verify.py | 3 +- tests/test_database.py | 2 +- 4 files changed, 61 insertions(+), 55 deletions(-) create mode 100644 eligibility_server/db/__init__.py diff --git a/eligibility_server/database.py b/eligibility_server/database.py index b0f68a40..c2a5705d 100644 --- a/eligibility_server/database.py +++ b/eligibility_server/database.py @@ -2,7 +2,6 @@ Simple hard-coded server database. """ -import ast import logging from flask_sqlalchemy import SQLAlchemy @@ -24,55 +23,3 @@ class User(db.Model): def __repr__(self): return "" % self.sub - - -class Database: - def __init__(self, hash=False): - """ - Initialize with database data and optionally lookup by hashing inputs - - @param hash: Hash object to lookup hashed inputs. False to lookup raw inputs. - """ - - self._hash = hash - if hash: - logger.debug(f"Database initialized with hash: {hash}") - else: - logger.debug("Database initialized without hashing") - - def check_user(self, sub: str, name: str, types: str) -> list: - """ - Check if the data matches a record in the database - - @param self: self - @param sub: sub to check for - @param name: name of user to check for - @param types: type of eligibility - - @return list of strings of types user is eligible for, or empty list - """ - - if len(types) < 1: - logger.debug("List of types to check was empty.") - return [] - - if self._hash: - sub = self._hash.hash_input(sub) - name = self._hash.hash_input(name) - - existing_user = User.query.filter_by(sub=sub, name=name).first() - if existing_user: - existing_user_types = ast.literal_eval(existing_user.types) - else: - existing_user_types = [] - - matching_types = set(existing_user_types) & set(types) - - if existing_user is None: - logger.debug("Database does not contain requested user.") - return [] - elif len(matching_types) < 1: - logger.debug(f"User's types do not contain any of the requested types: {types}") - return [] - else: - return list(matching_types) diff --git a/eligibility_server/db/__init__.py b/eligibility_server/db/__init__.py new file mode 100644 index 00000000..a948e9ee --- /dev/null +++ b/eligibility_server/db/__init__.py @@ -0,0 +1,58 @@ +import ast +import logging + +from ..database import User + +logger = logging.getLogger(__name__) + + +class Database: + def __init__(self, hash=False): + """ + Initialize with database data and optionally lookup by hashing inputs + + @param hash: Hash object to lookup hashed inputs. False to lookup raw inputs. + """ + + self._hash = hash + if hash: + logger.debug(f"Database initialized with hash: {hash}") + else: + logger.debug("Database initialized without hashing") + + def check_user(self, sub: str, name: str, types: str) -> list: + """ + Check if the data matches a record in the database + + @param self: self + @param sub: sub to check for + @param name: name of user to check for + @param types: type of eligibility + + @return list of strings of types user is eligible for, or empty list + """ + + if len(types) < 1: + logger.debug("List of types to check was empty.") + return [] + + if self._hash: + sub = self._hash.hash_input(sub) + name = self._hash.hash_input(name) + + existing_user = User.query.filter_by(sub=sub, name=name).first() + if existing_user: + existing_user_types = ast.literal_eval(existing_user.types) + else: + existing_user_types = [] + + matching_types = set(existing_user_types) & set(types) + + if existing_user is None: + logger.debug("Database does not contain requested user.") + return [] + elif len(matching_types) < 1: + logger.debug(f"User's types do not contain any of the requested types: {types}") + return [] + else: + return list(matching_types) diff --git a/eligibility_server/verify.py b/eligibility_server/verify.py index 7569551a..08bc9d94 100644 --- a/eligibility_server/verify.py +++ b/eligibility_server/verify.py @@ -13,7 +13,8 @@ from jwcrypto import jwe, jws, jwt from . import app, keypair -from .database import Database +from . import app +from .db import Database from .hash import Hash diff --git a/tests/test_database.py b/tests/test_database.py index 88516990..90fe9d49 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -4,7 +4,7 @@ import pytest -from eligibility_server.database import Database +from eligibility_server.db import Database from eligibility_server.hash import Hash From e246508f92df63c38cb26612131f0986febc1151 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Sat, 27 Aug 2022 01:38:49 +0000 Subject: [PATCH 03/19] refactor: move db and User into a models module inside db package --- eligibility_server/app.py | 4 ++-- eligibility_server/db/__init__.py | 2 +- eligibility_server/{database.py => db/models.py} | 4 ---- setup.py | 2 +- teardown.py | 2 +- 5 files changed, 5 insertions(+), 9 deletions(-) rename eligibility_server/{database.py => db/models.py} (91%) diff --git a/eligibility_server/app.py b/eligibility_server/app.py index 6813d99c..d4770c3c 100644 --- a/eligibility_server/app.py +++ b/eligibility_server/app.py @@ -9,7 +9,7 @@ from .verify import Verify from .keypair import get_server_public_key -from . import database +from .db import models app = Flask(__name__) app.config.from_object("eligibility_server.settings") @@ -67,7 +67,7 @@ def internal_server_error(error): api = Api(app) api.add_resource(Verify, "/verify") -database.init_app(app) +models.init_app(app) if __name__ == "__main__": app.run(host=app.config["HOST"], debug=app.config["DEBUG_MODE"], port="8000") # nosec diff --git a/eligibility_server/db/__init__.py b/eligibility_server/db/__init__.py index a948e9ee..6842170c 100644 --- a/eligibility_server/db/__init__.py +++ b/eligibility_server/db/__init__.py @@ -1,7 +1,7 @@ import ast import logging -from ..database import User +from .models import User logger = logging.getLogger(__name__) diff --git a/eligibility_server/database.py b/eligibility_server/db/models.py similarity index 91% rename from eligibility_server/database.py rename to eligibility_server/db/models.py index c2a5705d..deffd544 100644 --- a/eligibility_server/database.py +++ b/eligibility_server/db/models.py @@ -1,7 +1,3 @@ -""" -Simple hard-coded server database. -""" - import logging from flask_sqlalchemy import SQLAlchemy diff --git a/setup.py b/setup.py index db1e7853..4709f805 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from flask_sqlalchemy import inspect from eligibility_server.app import app -from eligibility_server.database import db, User +from eligibility_server.db.models import db, User logger = logging.getLogger("setup") diff --git a/teardown.py b/teardown.py index 244c5b78..32c0afb7 100644 --- a/teardown.py +++ b/teardown.py @@ -3,7 +3,7 @@ from flask_sqlalchemy import inspect from eligibility_server.app import app -from eligibility_server.database import db, User +from eligibility_server.db.models import db, User logger = logging.getLogger("teardown") From 24fb1ec61051794d8693021c0858a777162bb7d5 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Sat, 27 Aug 2022 02:02:28 +0000 Subject: [PATCH 04/19] refactor: convert setup script into click CLI command run it using `flask init-db` --- .github/workflows/run-tests.yml | 2 +- bin/init.sh | 2 +- docs/getting-started.md | 2 +- eligibility_server/db/models.py | 4 ++ setup.py => eligibility_server/db/setup.py | 63 +++++++++++----------- 5 files changed, 38 insertions(+), 35 deletions(-) rename setup.py => eligibility_server/db/setup.py (54%) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 5efd57f5..a7fb7a29 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -27,6 +27,6 @@ jobs: - name: Test with pytest run: | - python setup.py + flask init-db coverage run -m pytest coverage report -m diff --git a/bin/init.sh b/bin/init.sh index ebb5cc8d..09b149e5 100755 --- a/bin/init.sh +++ b/bin/init.sh @@ -3,4 +3,4 @@ set -eux # run database migrations -python setup.py +flask init-db diff --git a/docs/getting-started.md b/docs/getting-started.md index ac913556..338f4f08 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -89,7 +89,7 @@ python teardown.py To set up the database with a new import file or other configuration variables, after making any new environment variable changes, run: ```bash -python setup.py +flask init-db ``` ## Run and develop the Documentation diff --git a/eligibility_server/db/models.py b/eligibility_server/db/models.py index deffd544..7b5669a0 100644 --- a/eligibility_server/db/models.py +++ b/eligibility_server/db/models.py @@ -10,6 +10,10 @@ def init_app(app): db.init_app(app) + from .setup import init_db_command + + app.cli.add_command(init_db_command) + class User(db.Model): id = db.Column(db.Integer, primary_key=True) diff --git a/setup.py b/eligibility_server/db/setup.py similarity index 54% rename from setup.py rename to eligibility_server/db/setup.py index 4709f805..200d0356 100644 --- a/setup.py +++ b/eligibility_server/db/setup.py @@ -1,14 +1,30 @@ import csv import json -import logging +import click +from flask import current_app from flask_sqlalchemy import inspect -from eligibility_server.app import app -from eligibility_server.db.models import db, User +from .models import db, User -logger = logging.getLogger("setup") +@click.command("init-db") +def init_db_command(): + with current_app.app_context(): + inspector = inspect(db.engine) + + if inspector.get_table_names(): + click.echo("Tables already exist.") + if User.query.count() == 0: + import_users() + else: + click.echo("User table already has data.") + else: + click.echo("Creating table...") + db.create_all() + click.echo("Table created.") + + import_users() def import_users(): @@ -20,8 +36,8 @@ def import_users(): configurations: CSV_DELIMITER, CSV_NEWLINE, CSV_QUOTING, CSV_QUOTECHAR """ - file_path = app.config["IMPORT_FILE_PATH"] - logger.info(f"Importing users from {file_path}") + file_path = current_app.config["IMPORT_FILE_PATH"] + click.echo(f"Importing users from {file_path}") file_format = file_path.split(".")[-1] @@ -29,24 +45,24 @@ def import_users(): with open(file_path) as file: data = json.load(file)["users"] for user in data: - save_users(user, data[user][0], str(data[user][1])) + save_users(db, User, user, data[user][0], str(data[user][1])) elif file_format == "csv": - with open(file_path, newline=app.config["CSV_NEWLINE"], encoding="utf-8") as file: + with open(file_path, newline=current_app.config["CSV_NEWLINE"], encoding="utf-8") as file: data = csv.reader( file, - delimiter=app.config["CSV_DELIMITER"], - quoting=int(app.config["CSV_QUOTING"]), - quotechar=app.config["CSV_QUOTECHAR"], + delimiter=current_app.config["CSV_DELIMITER"], + quoting=int(current_app.config["CSV_QUOTING"]), + quotechar=current_app.config["CSV_QUOTECHAR"], ) for user in data: - save_users(user[0], user[1], user[2]) + save_users(db, User, user[0], user[1], user[2]) else: - logger.warning(f"File format is not supported: {file_format}") + click.echo(f"Warning: File format is not supported: {file_format}") - logger.info(f"Users added: {User.query.count()}") + click.echo(f"Users added: {User.query.count()}") -def save_users(sub: str, name: str, types: str): +def save_users(db, User, sub: str, name: str, types: str): """ Add users to the database User table @@ -58,20 +74,3 @@ def save_users(sub: str, name: str, types: str): item = User(sub=sub, name=name, types=types) db.session.add(item) db.session.commit() - - -if __name__ == "__main__": - with app.app_context(): - inspector = inspect(db.engine) - - if inspector.get_table_names(): - logger.info("Tables already exist.") - if User.query.count() == 0: - import_users() - else: - logger.info("User table already has data.") - else: - logger.info("Creating table...") - db.create_all() - logger.info("Table created.") - import_users() From d475f6e22e75a82385e0a8035b6208669c4fd80c Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Sat, 27 Aug 2022 02:09:37 +0000 Subject: [PATCH 05/19] refactor: convert teardown script into click CLI command run it using `flask drop-db` --- docs/getting-started.md | 2 +- eligibility_server/db/models.py | 2 ++ eligibility_server/db/teardown.py | 24 ++++++++++++++++++++++++ teardown.py | 25 ------------------------- 4 files changed, 27 insertions(+), 26 deletions(-) create mode 100644 eligibility_server/db/teardown.py delete mode 100644 teardown.py diff --git a/docs/getting-started.md b/docs/getting-started.md index 338f4f08..3a562d63 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -83,7 +83,7 @@ In testing the database, you may need to teardown the database and restart a dat The teardown script removes all users and drops the database. To tear down the database, run: ```bash -python teardown.py +flask drop-db ``` To set up the database with a new import file or other configuration variables, after making any new environment variable changes, run: diff --git a/eligibility_server/db/models.py b/eligibility_server/db/models.py index 7b5669a0..020ae5d8 100644 --- a/eligibility_server/db/models.py +++ b/eligibility_server/db/models.py @@ -11,8 +11,10 @@ def init_app(app): db.init_app(app) from .setup import init_db_command + from .teardown import drop_db_command app.cli.add_command(init_db_command) + app.cli.add_command(drop_db_command) class User(db.Model): diff --git a/eligibility_server/db/teardown.py b/eligibility_server/db/teardown.py new file mode 100644 index 00000000..4e5bc37a --- /dev/null +++ b/eligibility_server/db/teardown.py @@ -0,0 +1,24 @@ +import click +from flask import current_app +from flask_sqlalchemy import inspect + +from .models import db, User + + +@click.command("drop-db") +def drop_db_command(): + with current_app.app_context(): + inspector = inspect(db.engine) + + if inspector.get_table_names(): + try: + click.echo(f"Users to be deleted: {User.query.count()}") + User.query.delete() + db.session.commit() + except Exception as e: + click.echo("Failed to query for Users", e) + + db.drop_all() + click.echo("Database dropped.") + else: + click.echo("Database does not exist.") diff --git a/teardown.py b/teardown.py deleted file mode 100644 index 32c0afb7..00000000 --- a/teardown.py +++ /dev/null @@ -1,25 +0,0 @@ -import logging - -from flask_sqlalchemy import inspect - -from eligibility_server.app import app -from eligibility_server.db.models import db, User - -logger = logging.getLogger("teardown") - -if __name__ == "__main__": - with app.app_context(): - inspector = inspect(db.engine) - - if inspector.get_table_names(): - try: - logger.info(f"Users to be deleted: {User.query.count()}") - User.query.delete() - db.session.commit() - except Exception as e: - logger.warning("Failed to query for Users", e) - - db.drop_all() - logger.info("Database dropped.") - else: - logger.info("Database does not exist.") From 7a857d92ca500cbdc5bc3aac5744c21a483797bb Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Tue, 30 Aug 2022 21:47:52 +0000 Subject: [PATCH 06/19] refactor: verify module uses current_app instead of actual app object --- eligibility_server/verify.py | 30 +++++++++++++++--------------- tests/test_verify.py | 4 ++-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/eligibility_server/verify.py b/eligibility_server/verify.py index 08bc9d94..19fc41c5 100644 --- a/eligibility_server/verify.py +++ b/eligibility_server/verify.py @@ -8,12 +8,11 @@ import re import time -from flask import abort +from flask import abort, current_app from flask_restful import Resource, reqparse from jwcrypto import jwe, jws, jwt -from . import app, keypair -from . import app +from . import keypair from .db import Database from .hash import Hash @@ -27,8 +26,8 @@ def __init__(self): self.client_public_key = keypair.get_client_public_key() self.server_private_key = keypair.get_server_private_key() - if app.app.config["INPUT_HASH_ALGO"] != "": - hash = Hash(app.app.config["INPUT_HASH_ALGO"]) + if current_app.config["INPUT_HASH_ALGO"] != "": + hash = Hash(current_app.config["INPUT_HASH_ALGO"]) self._db = Database(hash=hash) else: self._db = Database() @@ -36,18 +35,19 @@ def __init__(self): def _check_headers(self): """Ensure correct request headers.""" req_parser = reqparse.RequestParser() - req_parser.add_argument(app.app.config["TOKEN_HEADER"], location="headers", required=True) - req_parser.add_argument(app.app.config["AUTH_HEADER"], location="headers", required=True) + req_parser.add_argument(current_app.config["TOKEN_HEADER"], location="headers", required=True) + req_parser.add_argument(current_app.config["AUTH_HEADER"], location="headers", required=True) headers = req_parser.parse_args() + # verify auth_header's value - if headers.get(app.app.config["AUTH_HEADER"]) == app.app.config["AUTH_TOKEN"]: + if headers.get(current_app.config["AUTH_HEADER"]) == current_app.config["AUTH_TOKEN"]: return headers else: return False def _get_token(self, headers): """Get the token from request headers""" - token = headers.get(app.app.config["TOKEN_HEADER"], "").split(" ") + token = headers.get(current_app.config["TOKEN_HEADER"], "").split(" ") if len(token) == 2: return token[1] elif len(token) == 1: @@ -60,12 +60,12 @@ def _get_token_payload(self, token): """Decode a token (JWE(JWS)).""" try: # decrypt - decrypted_token = jwe.JWE(algs=[app.app.config["JWE_ENCRYPTION_ALG"], app.app.config["JWE_CEK_ENC"]]) + decrypted_token = jwe.JWE(algs=[current_app.config["JWE_ENCRYPTION_ALG"], current_app.config["JWE_CEK_ENC"]]) decrypted_token.deserialize(token, key=self.server_private_key) decrypted_payload = str(decrypted_token.payload, "utf-8") # verify signature signed_token = jws.JWS() - signed_token.deserialize(decrypted_payload, key=self.client_public_key, alg=app.app.config["JWS_SIGNING_ALG"]) + signed_token.deserialize(decrypted_payload, key=self.client_public_key, alg=current_app.config["JWS_SIGNING_ALG"]) # return final payload payload = str(signed_token.payload, "utf-8") return json.loads(payload) @@ -76,12 +76,12 @@ def _get_token_payload(self, token): def _make_token(self, payload): """Wrap payload in a signed and encrypted JWT for response.""" # sign the payload with server's private key - header = {"typ": "JWS", "alg": app.app.config["JWS_SIGNING_ALG"]} + header = {"typ": "JWS", "alg": current_app.config["JWS_SIGNING_ALG"]} signed_token = jwt.JWT(header=header, claims=payload) signed_token.make_signed_token(self.server_private_key) signed_payload = signed_token.serialize() # encrypt the signed payload with client's public key - header = {"typ": "JWE", "alg": app.app.config["JWE_ENCRYPTION_ALG"], "enc": app.app.config["JWE_CEK_ENC"]} + header = {"typ": "JWE", "alg": current_app.config["JWE_ENCRYPTION_ALG"], "enc": current_app.config["JWE_CEK_ENC"]} encrypted_token = jwt.JWT(header=header, claims=signed_payload) encrypted_token.make_encrypted_token(self.client_public_key) return encrypted_token.serialize() @@ -92,11 +92,11 @@ def _get_response(self, token_payload): sub, name, eligibility = token_payload["sub"], token_payload["name"], list(token_payload["eligibility"]) resp_payload = dict( jti=token_payload["jti"], - iss=app.app.config["APP_NAME"], + iss=current_app.config["APP_NAME"], iat=int(datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).timestamp()), ) # sub format check - if re.match(app.app.config["SUB_FORMAT_REGEX"], sub): + if re.match(current_app.config["SUB_FORMAT_REGEX"], sub): # eligibility check against db resp_payload["eligibility"] = self._db.check_user(sub, name, eligibility) code = 200 diff --git a/tests/test_verify.py b/tests/test_verify.py index d7964daf..2eb5ec84 100644 --- a/tests/test_verify.py +++ b/tests/test_verify.py @@ -29,7 +29,7 @@ def test_Verify_client_get_bad_request(mocker, client): @pytest.mark.usefixtures("flask") def test_Verify_get_response_sub_format_match(mocker): mocked_config = {"SUB_FORMAT_REGEX": r"^[A-Z]\d{7}$", "INPUT_HASH_ALGO": ""} - mocker.patch.dict("eligibility_server.verify.app.app.config", mocked_config) + mocker.patch.dict("eligibility_server.verify.current_app.config", mocked_config) token_payload = json.loads(json.dumps(dict(sub="A1234567", name="Garcia", eligibility=["type1"], jti=str(uuid.uuid4())))) @@ -40,7 +40,7 @@ def test_Verify_get_response_sub_format_match(mocker): def test_Verify_get_response_sub_format_no_match(mocker): mocked_config = {"SUB_FORMAT_REGEX": r"^[A-Z]\d{7}$", "INPUT_HASH_ALGO": ""} - mocker.patch.dict("eligibility_server.verify.app.app.config", mocked_config) + mocker.patch.dict("eligibility_server.verify.current_app.config", mocked_config) # "sub" value does not match the format regex token_payload = json.loads(json.dumps(dict(sub="nomatch", name="Garcia", eligibility=["type1"], jti=str(uuid.uuid4())))) From a23143b133ac0e6fb9374dbb788642025fba5073 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Tue, 30 Aug 2022 23:43:44 +0000 Subject: [PATCH 07/19] feat: make package importable through setup.py --- .gitignore | 1 + setup.py | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 setup.py diff --git a/.gitignore b/.gitignore index 139ee80b..a5aec653 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ .env config/* !config/sample.py +eligibility_server.egg-info diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..60a31b44 --- /dev/null +++ b/setup.py @@ -0,0 +1,10 @@ +from setuptools import setup + +setup( + name="eligibility_server", + packages=["eligibility_server"], + include_package_data=True, + install_requires=[ + "flask", + ], +) From 2d65a634f385f4a5b8ed8b2cbe7a6febc37fcdeb Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Wed, 31 Aug 2022 00:55:06 +0000 Subject: [PATCH 08/19] ci: add variable for use in run-tests workflow --- .env.sample | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.sample b/.env.sample index 179bf4f7..3a4bbbd5 100644 --- a/.env.sample +++ b/.env.sample @@ -1 +1,2 @@ +FLASK_APP=eligibility_server/app.py ELIGIBILITY_SERVER_SETTINGS=../config/sample.py From 75dea8ed2b3e9c1ea6d8c306b412baa5333f65e1 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Thu, 1 Sep 2022 22:37:59 +0000 Subject: [PATCH 09/19] docs: correct some details about init script --- docs/getting-started.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 3a562d63..871dd032 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -65,8 +65,7 @@ Once you clone the repository locally, open it within VS Code, which will prompt 2. Start the `eligibility-server` Flask app and database with `F5` 3. Now you can run tests from the container. -Starting the Dev Container will run `bin/init.sh`, which runs `setup.py` and starts the Flask app. The `setup.py` script creates the database and imports and saves users -based on the configured settings. +Starting the Dev Container will run `bin/init.sh`, which runs a command to initialize the database. More specifically, it creates the database and imports and saves users based on the configured settings. ## Run tests @@ -80,7 +79,7 @@ The test suite runs against every pull request via a GitHub Action. In testing the database, you may need to teardown the database and restart a database from scratch. -The teardown script removes all users and drops the database. To tear down the database, run: +The command below will remove all users and drop the database: ```bash flask drop-db From 7de32c107189d7ef694ae65317e57578ecb56469 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Fri, 2 Sep 2022 19:48:06 +0000 Subject: [PATCH 10/19] chore: remove unnecessary parameters leftover from refactoring --- eligibility_server/db/setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/eligibility_server/db/setup.py b/eligibility_server/db/setup.py index 200d0356..ddc4df42 100644 --- a/eligibility_server/db/setup.py +++ b/eligibility_server/db/setup.py @@ -45,7 +45,7 @@ def import_users(): with open(file_path) as file: data = json.load(file)["users"] for user in data: - save_users(db, User, user, data[user][0], str(data[user][1])) + save_users(user, data[user][0], str(data[user][1])) elif file_format == "csv": with open(file_path, newline=current_app.config["CSV_NEWLINE"], encoding="utf-8") as file: data = csv.reader( @@ -55,14 +55,14 @@ def import_users(): quotechar=current_app.config["CSV_QUOTECHAR"], ) for user in data: - save_users(db, User, user[0], user[1], user[2]) + save_users(user[0], user[1], user[2]) else: click.echo(f"Warning: File format is not supported: {file_format}") click.echo(f"Users added: {User.query.count()}") -def save_users(db, User, sub: str, name: str, types: str): +def save_users(sub: str, name: str, types: str): """ Add users to the database User table From 5458a4236ff676ce422f52b235a972e233f06cd3 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Fri, 2 Sep 2022 20:01:13 +0000 Subject: [PATCH 11/19] refactor: move init_app to module initialization temporarily move Database class to separate module to avoid circular import --- eligibility_server/app.py | 4 +- eligibility_server/db/__init__.py | 64 ++++++------------------------- eligibility_server/db/database.py | 59 ++++++++++++++++++++++++++++ eligibility_server/db/models.py | 18 +-------- eligibility_server/verify.py | 2 +- tests/test_database.py | 2 +- 6 files changed, 76 insertions(+), 73 deletions(-) create mode 100644 eligibility_server/db/database.py diff --git a/eligibility_server/app.py b/eligibility_server/app.py index d4770c3c..8c22d4ce 100644 --- a/eligibility_server/app.py +++ b/eligibility_server/app.py @@ -7,9 +7,9 @@ from flask_restful import Api from flask.logging import default_handler +from . import db from .verify import Verify from .keypair import get_server_public_key -from .db import models app = Flask(__name__) app.config.from_object("eligibility_server.settings") @@ -67,7 +67,7 @@ def internal_server_error(error): api = Api(app) api.add_resource(Verify, "/verify") -models.init_app(app) +db.init_app(app) if __name__ == "__main__": app.run(host=app.config["HOST"], debug=app.config["DEBUG_MODE"], port="8000") # nosec diff --git a/eligibility_server/db/__init__.py b/eligibility_server/db/__init__.py index 6842170c..95b92ae1 100644 --- a/eligibility_server/db/__init__.py +++ b/eligibility_server/db/__init__.py @@ -1,58 +1,18 @@ -import ast import logging -from .models import User +from flask_sqlalchemy import SQLAlchemy logger = logging.getLogger(__name__) -class Database: - def __init__(self, hash=False): - """ - Initialize with database data and optionally lookup by hashing inputs - - @param hash: Hash object to lookup hashed inputs. False to lookup raw inputs. - """ - - self._hash = hash - if hash: - logger.debug(f"Database initialized with hash: {hash}") - else: - logger.debug("Database initialized without hashing") - - def check_user(self, sub: str, name: str, types: str) -> list: - """ - Check if the data matches a record in the database - - @param self: self - @param sub: sub to check for - @param name: name of user to check for - @param types: type of eligibility - - @return list of strings of types user is eligible for, or empty list - """ - - if len(types) < 1: - logger.debug("List of types to check was empty.") - return [] - - if self._hash: - sub = self._hash.hash_input(sub) - name = self._hash.hash_input(name) - - existing_user = User.query.filter_by(sub=sub, name=name).first() - if existing_user: - existing_user_types = ast.literal_eval(existing_user.types) - else: - existing_user_types = [] - - matching_types = set(existing_user_types) & set(types) - - if existing_user is None: - logger.debug("Database does not contain requested user.") - return [] - elif len(matching_types) < 1: - logger.debug(f"User's types do not contain any of the requested types: {types}") - return [] - else: - return list(matching_types) +db = SQLAlchemy() + + +def init_app(app): + db.init_app(app) + + from .setup import init_db_command + from .teardown import drop_db_command + + app.cli.add_command(init_db_command) + app.cli.add_command(drop_db_command) diff --git a/eligibility_server/db/database.py b/eligibility_server/db/database.py new file mode 100644 index 00000000..2edcc65e --- /dev/null +++ b/eligibility_server/db/database.py @@ -0,0 +1,59 @@ +import ast +import logging + +from .models import User + + +logger = logging.getLogger(__name__) + + +class Database: + def __init__(self, hash=False): + """ + Initialize with database data and optionally lookup by hashing inputs + + @param hash: Hash object to lookup hashed inputs. False to lookup raw inputs. + """ + + self._hash = hash + if hash: + logger.debug(f"Database initialized with hash: {hash}") + else: + logger.debug("Database initialized without hashing") + + def check_user(self, sub: str, name: str, types: str) -> list: + """ + Check if the data matches a record in the database + + @param self: self + @param sub: sub to check for + @param name: name of user to check for + @param types: type of eligibility + + @return list of strings of types user is eligible for, or empty list + """ + + if len(types) < 1: + logger.debug("List of types to check was empty.") + return [] + + if self._hash: + sub = self._hash.hash_input(sub) + name = self._hash.hash_input(name) + + existing_user = User.query.filter_by(sub=sub, name=name).first() + if existing_user: + existing_user_types = ast.literal_eval(existing_user.types) + else: + existing_user_types = [] + + matching_types = set(existing_user_types) & set(types) + + if existing_user is None: + logger.debug("Database does not contain requested user.") + return [] + elif len(matching_types) < 1: + logger.debug(f"User's types do not contain any of the requested types: {types}") + return [] + else: + return list(matching_types) diff --git a/eligibility_server/db/models.py b/eligibility_server/db/models.py index 020ae5d8..4dc85eb8 100644 --- a/eligibility_server/db/models.py +++ b/eligibility_server/db/models.py @@ -1,20 +1,4 @@ -import logging - -from flask_sqlalchemy import SQLAlchemy - -logger = logging.getLogger(__name__) - -db = SQLAlchemy() - - -def init_app(app): - db.init_app(app) - - from .setup import init_db_command - from .teardown import drop_db_command - - app.cli.add_command(init_db_command) - app.cli.add_command(drop_db_command) +from . import db class User(db.Model): diff --git a/eligibility_server/verify.py b/eligibility_server/verify.py index 19fc41c5..8baf514b 100644 --- a/eligibility_server/verify.py +++ b/eligibility_server/verify.py @@ -13,7 +13,7 @@ from jwcrypto import jwe, jws, jwt from . import keypair -from .db import Database +from .db.database import Database from .hash import Hash diff --git a/tests/test_database.py b/tests/test_database.py index 90fe9d49..332a0751 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -4,7 +4,7 @@ import pytest -from eligibility_server.db import Database +from eligibility_server.db.database import Database from eligibility_server.hash import Hash From 1385b77cbfd72565f29c130e91075833efd24f42 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Fri, 2 Sep 2022 20:12:31 +0000 Subject: [PATCH 12/19] refactor: move database commands into single file --- eligibility_server/db/__init__.py | 3 +-- eligibility_server/db/setup.py | 19 +++++++++++++++++++ eligibility_server/db/teardown.py | 24 ------------------------ 3 files changed, 20 insertions(+), 26 deletions(-) delete mode 100644 eligibility_server/db/teardown.py diff --git a/eligibility_server/db/__init__.py b/eligibility_server/db/__init__.py index 95b92ae1..1b4d7001 100644 --- a/eligibility_server/db/__init__.py +++ b/eligibility_server/db/__init__.py @@ -11,8 +11,7 @@ def init_app(app): db.init_app(app) - from .setup import init_db_command - from .teardown import drop_db_command + from .setup import init_db_command, drop_db_command app.cli.add_command(init_db_command) app.cli.add_command(drop_db_command) diff --git a/eligibility_server/db/setup.py b/eligibility_server/db/setup.py index ddc4df42..cc2d18d3 100644 --- a/eligibility_server/db/setup.py +++ b/eligibility_server/db/setup.py @@ -74,3 +74,22 @@ def save_users(sub: str, name: str, types: str): item = User(sub=sub, name=name, types=types) db.session.add(item) db.session.commit() + + +@click.command("drop-db") +def drop_db_command(): + with current_app.app_context(): + inspector = inspect(db.engine) + + if inspector.get_table_names(): + try: + click.echo(f"Users to be deleted: {User.query.count()}") + User.query.delete() + db.session.commit() + except Exception as e: + click.echo("Failed to query for Users", e) + + db.drop_all() + click.echo("Database dropped.") + else: + click.echo("Database does not exist.") diff --git a/eligibility_server/db/teardown.py b/eligibility_server/db/teardown.py deleted file mode 100644 index 4e5bc37a..00000000 --- a/eligibility_server/db/teardown.py +++ /dev/null @@ -1,24 +0,0 @@ -import click -from flask import current_app -from flask_sqlalchemy import inspect - -from .models import db, User - - -@click.command("drop-db") -def drop_db_command(): - with current_app.app_context(): - inspector = inspect(db.engine) - - if inspector.get_table_names(): - try: - click.echo(f"Users to be deleted: {User.query.count()}") - User.query.delete() - db.session.commit() - except Exception as e: - click.echo("Failed to query for Users", e) - - db.drop_all() - click.echo("Database dropped.") - else: - click.echo("Database does not exist.") From ae836f08fa58f09ba9164845b58dfa78fcdbe461 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Fri, 2 Sep 2022 22:01:12 +0000 Subject: [PATCH 13/19] chore(metadata): flesh out package metadata in setup script --- setup.py | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index 60a31b44..32eba2c2 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,36 @@ -from setuptools import setup +from setuptools import find_packages, setup + + +with open("requirements.txt") as f: + install_requires = f.read().strip().split("\n") + +with open("README.md") as f: + long_description = f.read() + + +CLASSIFIERS = [ + "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", +] setup( - name="eligibility_server", - packages=["eligibility_server"], + name="eligibility-server", + description="Server implementation of the Eligibility Verification API", + long_description=long_description, + long_description_content_type="text/markdown", + python_requires=">=3.7", + classifiers=CLASSIFIERS, + project_urls={ + "Source": "https://github.com/cal-itp/eligibility-server", + "Tracker": "https://github.com/cal-itp/eligibility-server/issues", + }, + packages=find_packages(), include_package_data=True, - install_requires=[ - "flask", - ], + install_requires=install_requires, + license="AGPL-3.0", ) From a8989263acd31befacb1da5922198db77dbcbda4 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Wed, 7 Sep 2022 02:29:19 +0000 Subject: [PATCH 14/19] refactor: move check_user logic into Verify remove unnecessary Database class, and port test_database to use Verify. --- eligibility_server/db/database.py | 59 ------------------------------- eligibility_server/verify.py | 51 +++++++++++++++++++++++--- tests/test_database.py | 39 ++++++++++---------- 3 files changed, 64 insertions(+), 85 deletions(-) delete mode 100644 eligibility_server/db/database.py diff --git a/eligibility_server/db/database.py b/eligibility_server/db/database.py deleted file mode 100644 index 2edcc65e..00000000 --- a/eligibility_server/db/database.py +++ /dev/null @@ -1,59 +0,0 @@ -import ast -import logging - -from .models import User - - -logger = logging.getLogger(__name__) - - -class Database: - def __init__(self, hash=False): - """ - Initialize with database data and optionally lookup by hashing inputs - - @param hash: Hash object to lookup hashed inputs. False to lookup raw inputs. - """ - - self._hash = hash - if hash: - logger.debug(f"Database initialized with hash: {hash}") - else: - logger.debug("Database initialized without hashing") - - def check_user(self, sub: str, name: str, types: str) -> list: - """ - Check if the data matches a record in the database - - @param self: self - @param sub: sub to check for - @param name: name of user to check for - @param types: type of eligibility - - @return list of strings of types user is eligible for, or empty list - """ - - if len(types) < 1: - logger.debug("List of types to check was empty.") - return [] - - if self._hash: - sub = self._hash.hash_input(sub) - name = self._hash.hash_input(name) - - existing_user = User.query.filter_by(sub=sub, name=name).first() - if existing_user: - existing_user_types = ast.literal_eval(existing_user.types) - else: - existing_user_types = [] - - matching_types = set(existing_user_types) & set(types) - - if existing_user is None: - logger.debug("Database does not contain requested user.") - return [] - elif len(matching_types) < 1: - logger.debug(f"User's types do not contain any of the requested types: {types}") - return [] - else: - return list(matching_types) diff --git a/eligibility_server/verify.py b/eligibility_server/verify.py index 8baf514b..7f50ce85 100644 --- a/eligibility_server/verify.py +++ b/eligibility_server/verify.py @@ -2,6 +2,7 @@ Eligibility Verification route """ +import ast import datetime import json import logging @@ -13,7 +14,7 @@ from jwcrypto import jwe, jws, jwt from . import keypair -from .db.database import Database +from .db.models import User from .hash import Hash @@ -22,15 +23,18 @@ class Verify(Resource): def __init__(self): - """Initialize Verify class with a keypair and Database""" + """Initialize Verify class with a keypair and hash""" self.client_public_key = keypair.get_client_public_key() self.server_private_key = keypair.get_server_private_key() if current_app.config["INPUT_HASH_ALGO"] != "": hash = Hash(current_app.config["INPUT_HASH_ALGO"]) - self._db = Database(hash=hash) + logger.debug(f"Verify initialized with hash: {hash}") else: - self._db = Database() + hash = None + logger.debug("Verify initialized without hashing") + + self.hash = hash def _check_headers(self): """Ensure correct request headers.""" @@ -98,7 +102,7 @@ def _get_response(self, token_payload): # sub format check if re.match(current_app.config["SUB_FORMAT_REGEX"], sub): # eligibility check against db - resp_payload["eligibility"] = self._db.check_user(sub, name, eligibility) + resp_payload["eligibility"] = self._check_user(sub, name, eligibility) code = 200 else: resp_payload["error"] = {"sub": "invalid"} @@ -112,6 +116,43 @@ def _get_response(self, token_payload): logger.error(f"Error: {ex}") abort(500, description="Internal server error") + def _check_user(self, sub, name, types): + """ + Check if the data matches a record in the database + + @param self: self + @param sub: sub to check for + @param name: name of user to check for + @param types: type of eligibility + + @return list of strings of types user is eligible for, or empty list + """ + + if len(types) < 1: + logger.debug("List of types to check was empty.") + return [] + + if self.hash: + sub = self.hash.hash_input(sub) + name = self.hash.hash_input(name) + + existing_user = User.query.filter_by(sub=sub, name=name).first() + if existing_user: + existing_user_types = ast.literal_eval(existing_user.types) + else: + existing_user_types = [] + + matching_types = set(existing_user_types) & set(types) + + if existing_user is None: + logger.debug("Database does not contain requested user.") + return [] + elif len(matching_types) < 1: + logger.debug(f"User's types do not contain any of the requested types: {types}") + return [] + else: + return list(matching_types) + def get(self): """Respond to a verification request.""" # introduce small fake delay diff --git a/tests/test_database.py b/tests/test_database.py index 332a0751..6788b383 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -4,31 +4,28 @@ import pytest -from eligibility_server.db.database import Database -from eligibility_server.hash import Hash +from eligibility_server.verify import Verify test_data = [ - (Database(), "A1234567", "Garcia", ["type1"], ["type1"]), # This sub/name pair is in the database - (Database(), "A1234567", "Garcia", ["type2"], []), # This sub/name pair does not have "type2" in its associated array - (Database(), "A1234567", "Aaron", ["type1"], []), # This sub/name pair does not exist - (Database(), "G7778889", "Thomas", ["type1"], []), # User not in database - (Database(Hash("sha256")), "A1234568", "Garcia", ["type1"], ["type1"]), # Correct sub/name pair and correct hash algo type - (Database(Hash("sha256")), "A1234568", "Garcia", ["type2"], []), # This sub/name pair does not have "type2" in its array - (Database(Hash("sha256")), "A1234568", "Aaron", ["type1"], []), # This sub/name pair does not exist - (Database(Hash("sha256")), "G7778889", "Thomas", ["type1"], []), # name does not exist - (Database(Hash("sha512")), "D4567891", "James", ["type1"], ["type1"]), # Specific hash algo type - (Database(Hash("sha256")), "D4567891", "James", ["type1"], []), # Wrong hash algo type + ("", "A1234567", "Garcia", ["type1"], ["type1"]), # This sub/name pair is in the database + ("", "A1234567", "Garcia", ["type2"], []), # This sub/name pair does not have "type2" in its associated array + ("", "A1234567", "Aaron", ["type1"], []), # This sub/name pair does not exist + ("", "G7778889", "Thomas", ["type1"], []), # User not in Hash + ("sha256", "A1234568", "Garcia", ["type1"], ["type1"]), # Correct sub/name pair and correct hash algo type + ("sha256", "A1234568", "Garcia", ["type2"], []), # This sub/name pair does not have "type2" in its array + ("sha256", "A1234568", "Aaron", ["type1"], []), # This sub/name pair does not exist + ("sha256", "G7778889", "Thomas", ["type1"], []), # name does not exist + ("sha512", "D4567891", "James", ["type1"], ["type1"]), # Specific hash algo type + ("sha256", "D4567891", "James", ["type1"], []), # Wrong hash algo type ] -def test_database_init_default(): - database = Database() - - assert database._hash is False - - @pytest.mark.usefixtures("flask") -@pytest.mark.parametrize("db, sub, name, types, expected", test_data) -def test_database_check_user(db, sub, name, types, expected): - assert db.check_user(sub, name, types) == expected +@pytest.mark.parametrize("hash, sub, name, types, expected", test_data) +def test_check_user(mocker, hash, sub, name, types, expected): + mocked_config = {"INPUT_HASH_ALGO": hash} + mocker.patch.dict("eligibility_server.verify.current_app.config", mocked_config) + + verify = Verify() + assert verify._check_user(sub, name, types) == expected From 84815067f8455bd505fc827e21a933743e5d75aa Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Wed, 7 Sep 2022 03:30:14 +0000 Subject: [PATCH 15/19] refactor(tests): move ported tests into more appropriate file --- tests/test_database.py | 31 ------------------------------- tests/test_verify.py | 25 +++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 31 deletions(-) delete mode 100644 tests/test_database.py diff --git a/tests/test_database.py b/tests/test_database.py deleted file mode 100644 index 6788b383..00000000 --- a/tests/test_database.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Test database class and methods -""" -import pytest - - -from eligibility_server.verify import Verify - - -test_data = [ - ("", "A1234567", "Garcia", ["type1"], ["type1"]), # This sub/name pair is in the database - ("", "A1234567", "Garcia", ["type2"], []), # This sub/name pair does not have "type2" in its associated array - ("", "A1234567", "Aaron", ["type1"], []), # This sub/name pair does not exist - ("", "G7778889", "Thomas", ["type1"], []), # User not in Hash - ("sha256", "A1234568", "Garcia", ["type1"], ["type1"]), # Correct sub/name pair and correct hash algo type - ("sha256", "A1234568", "Garcia", ["type2"], []), # This sub/name pair does not have "type2" in its array - ("sha256", "A1234568", "Aaron", ["type1"], []), # This sub/name pair does not exist - ("sha256", "G7778889", "Thomas", ["type1"], []), # name does not exist - ("sha512", "D4567891", "James", ["type1"], ["type1"]), # Specific hash algo type - ("sha256", "D4567891", "James", ["type1"], []), # Wrong hash algo type -] - - -@pytest.mark.usefixtures("flask") -@pytest.mark.parametrize("hash, sub, name, types, expected", test_data) -def test_check_user(mocker, hash, sub, name, types, expected): - mocked_config = {"INPUT_HASH_ALGO": hash} - mocker.patch.dict("eligibility_server.verify.current_app.config", mocked_config) - - verify = Verify() - assert verify._check_user(sub, name, types) == expected diff --git a/tests/test_verify.py b/tests/test_verify.py index 2eb5ec84..7c92a0ba 100644 --- a/tests/test_verify.py +++ b/tests/test_verify.py @@ -55,3 +55,28 @@ def test_Verify_init_keypair(): assert verify.client_public_key assert verify.server_private_key + + +# todo: this should really be driven by a test database instead +test_data = [ + ("", "A1234567", "Garcia", ["type1"], ["type1"]), # This sub/name pair is in the database + ("", "A1234567", "Garcia", ["type2"], []), # This sub/name pair does not have "type2" in its associated array + ("", "A1234567", "Aaron", ["type1"], []), # This sub/name pair does not exist + ("", "G7778889", "Thomas", ["type1"], []), # User not in Hash + ("sha256", "A1234568", "Garcia", ["type1"], ["type1"]), # Correct sub/name pair and correct hash algo type + ("sha256", "A1234568", "Garcia", ["type2"], []), # This sub/name pair does not have "type2" in its array + ("sha256", "A1234568", "Aaron", ["type1"], []), # This sub/name pair does not exist + ("sha256", "G7778889", "Thomas", ["type1"], []), # name does not exist + ("sha512", "D4567891", "James", ["type1"], ["type1"]), # Specific hash algo type + ("sha256", "D4567891", "James", ["type1"], []), # Wrong hash algo type +] + + +@pytest.mark.usefixtures("flask") +@pytest.mark.parametrize("hash, sub, name, types, expected", test_data) +def test_check_user(mocker, hash, sub, name, types, expected): + mocked_config = {"INPUT_HASH_ALGO": hash} + mocker.patch.dict("eligibility_server.verify.current_app.config", mocked_config) + + verify = Verify() + assert verify._check_user(sub, name, types) == expected From 2a0669a5f8c0a9a0bf9916e98d9ec95c2f14e440 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Wed, 7 Sep 2022 02:53:51 +0000 Subject: [PATCH 16/19] chore: switch to using absolute imports --- eligibility_server/app.py | 6 +++--- eligibility_server/db/models.py | 2 +- eligibility_server/db/setup.py | 2 +- eligibility_server/keypair.py | 2 +- eligibility_server/verify.py | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/eligibility_server/app.py b/eligibility_server/app.py index 8c22d4ce..255998f2 100644 --- a/eligibility_server/app.py +++ b/eligibility_server/app.py @@ -7,9 +7,9 @@ from flask_restful import Api from flask.logging import default_handler -from . import db -from .verify import Verify -from .keypair import get_server_public_key +from eligibility_server import db +from eligibility_server.verify import Verify +from eligibility_server.keypair import get_server_public_key app = Flask(__name__) app.config.from_object("eligibility_server.settings") diff --git a/eligibility_server/db/models.py b/eligibility_server/db/models.py index 4dc85eb8..8de8eaa4 100644 --- a/eligibility_server/db/models.py +++ b/eligibility_server/db/models.py @@ -1,4 +1,4 @@ -from . import db +from eligibility_server.db import db class User(db.Model): diff --git a/eligibility_server/db/setup.py b/eligibility_server/db/setup.py index cc2d18d3..c5df06f1 100644 --- a/eligibility_server/db/setup.py +++ b/eligibility_server/db/setup.py @@ -5,7 +5,7 @@ from flask import current_app from flask_sqlalchemy import inspect -from .models import db, User +from eligibility_server.db.models import db, User @click.command("init-db") diff --git a/eligibility_server/keypair.py b/eligibility_server/keypair.py index fa586690..97c06678 100644 --- a/eligibility_server/keypair.py +++ b/eligibility_server/keypair.py @@ -3,7 +3,7 @@ from jwcrypto import jwk import requests -from . import app +from eligibility_server import app logger = logging.getLogger(__name__) diff --git a/eligibility_server/verify.py b/eligibility_server/verify.py index 7f50ce85..31f21180 100644 --- a/eligibility_server/verify.py +++ b/eligibility_server/verify.py @@ -13,9 +13,9 @@ from flask_restful import Resource, reqparse from jwcrypto import jwe, jws, jwt -from . import keypair -from .db.models import User -from .hash import Hash +from eligibility_server import keypair +from eligibility_server.db.models import User +from eligibility_server.hash import Hash logger = logging.getLogger(__name__) From 4d3bdb1568dc9725cbd9a972ce719df020685438 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Wed, 7 Sep 2022 03:05:53 +0000 Subject: [PATCH 17/19] feat(container): install package into app container --- Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dockerfile b/Dockerfile index e3c6c56f..5c9ada29 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,10 @@ RUN pip install --no-cache-dir -r requirements.txt COPY bin/ bin/ COPY eligibility_server/ eligibility_server/ COPY *.py . +COPY README.md . + +# install source as a package +RUN pip install -e . # start app ENTRYPOINT ["/bin/bash"] From 39aaa7b5eea1bc10e76a6624546b9b13e4f44ba0 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Wed, 7 Sep 2022 03:08:48 +0000 Subject: [PATCH 18/19] chore(metadata): remove unneeded specification of minor Python versions --- setup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.py b/setup.py index 32eba2c2..88dcabd8 100644 --- a/setup.py +++ b/setup.py @@ -13,9 +13,6 @@ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", ] setup( From fd1c91582538d1f8dd40e083f94d5b4ed7aaaa70 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Wed, 7 Sep 2022 03:37:39 +0000 Subject: [PATCH 19/19] chore: fix some docstrings for database commands --- eligibility_server/db/setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/eligibility_server/db/setup.py b/eligibility_server/db/setup.py index c5df06f1..a631561b 100644 --- a/eligibility_server/db/setup.py +++ b/eligibility_server/db/setup.py @@ -32,8 +32,8 @@ def import_users(): Imports user data to be added to database and saves user to database Users can be imported from either a JSON file or CSV file, as configured - with settings from environment variables. CSV files take extra setting - configurations: CSV_DELIMITER, CSV_NEWLINE, CSV_QUOTING, CSV_QUOTECHAR + with settings. CSV files take extra setting configurations: + CSV_DELIMITER, CSV_NEWLINE, CSV_QUOTING, CSV_QUOTECHAR """ file_path = current_app.config["IMPORT_FILE_PATH"] @@ -66,7 +66,7 @@ def save_users(sub: str, name: str, types: str): """ Add users to the database User table - @param sub - User's ID, not to be confused with Database row ID + @param sub - User's sub @param name - User's name @param types - Types of eligibilities, in a stringified list """