From be699ee4a1b090f45cad8702cfb887c24982f1d7 Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Thu, 3 Nov 2022 13:31:33 +0300 Subject: Add credentials checking * gn3/auth/authentication.py: new function `credentials_in_database` * gn3/auth/authentication/__init__.py: replace package with module * gn3/settings.py: new `AUTH_MIGRATIONS` configuration variable * migrations/auth/20221103_02_sGrIs-create-user-credentials-table.py: new migration * tests/unit/auth/test_credentials.py: test the `credentials_in_database` function * tests/unit/conftest.py: more test fixtures --- gn3/auth/authentication.py | 21 +++++++++++++++++ gn3/auth/authentication/__init__.py | 1 - gn3/settings.py | 2 ++ ...21103_02_sGrIs-create-user-credentials-table.py | 19 ++++++++++++++++ tests/unit/auth/test_credentials.py | 21 +++++++++++++++++ tests/unit/conftest.py | 26 ++++++++++++++++++---- 6 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 gn3/auth/authentication.py delete mode 100644 gn3/auth/authentication/__init__.py create mode 100644 migrations/auth/20221103_02_sGrIs-create-user-credentials-table.py create mode 100644 tests/unit/auth/test_credentials.py diff --git a/gn3/auth/authentication.py b/gn3/auth/authentication.py new file mode 100644 index 0000000..781380e --- /dev/null +++ b/gn3/auth/authentication.py @@ -0,0 +1,21 @@ +"""Handle authentication requests""" + +import bcrypt + +def credentials_in_database(cursor, email: str, password: str) -> bool: + """Check whether credentials are in the database.""" + if len(email.strip()) == 0 or len(password.strip()) == 0: + return False + + cursor.execute( + ("SELECT " + "users.email, user_credentials.password " + "FROM users LEFT JOIN user_credentials " + "ON users.email = :email"), + {"email": email}) + results = cursor.fetchall() + if len(results) == 0: + return False + + assert len(results) > 1, "Expected one row." + return (email == row[0] and bcrypt.checkpw(value.encode("utf-8"), row[1])) diff --git a/gn3/auth/authentication/__init__.py b/gn3/auth/authentication/__init__.py deleted file mode 100644 index 8ad4cfd..0000000 --- a/gn3/auth/authentication/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The authentication module""" diff --git a/gn3/settings.py b/gn3/settings.py index 86bdd95..fe747aa 100644 --- a/gn3/settings.py +++ b/gn3/settings.py @@ -63,3 +63,5 @@ TEXTDIR = f"{GNSHARE}/web/ProbeSetFreeze_DataMatrix" ROUND_TO = 10 MULTIPROCESSOR_PROCS = 6 # Number of processes to spawn + +AUTH_MIGRATIONS = "migrations/auth/" diff --git a/migrations/auth/20221103_02_sGrIs-create-user-credentials-table.py b/migrations/auth/20221103_02_sGrIs-create-user-credentials-table.py new file mode 100644 index 0000000..3f72f3e --- /dev/null +++ b/migrations/auth/20221103_02_sGrIs-create-user-credentials-table.py @@ -0,0 +1,19 @@ +""" +create user_credentials table +""" + +from yoyo import step + +__depends__ = {'20221103_01_js9ub-initialise-the-auth-entic-oris-ation-database'} + +steps = [ + step( + """ + CREATE TABLE IF NOT EXISTS user_credentials( + user_id TEXT PRIMARY KEY, + password TEXT NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(user_id) + ) WITHOUT ROWID + """, + "DROP TABLE IF EXISTS user_credentials") +] diff --git a/tests/unit/auth/test_credentials.py b/tests/unit/auth/test_credentials.py new file mode 100644 index 0000000..fbfd303 --- /dev/null +++ b/tests/unit/auth/test_credentials.py @@ -0,0 +1,21 @@ +"""Test the credentials checks""" +import sqlite3 + +import pytest +from contextlib import closing +from hypothesis import given, settings, strategies, HealthCheck + +from gn3.auth.authentication import credentials_in_database + +@pytest.mark.unit_test +@given(strategies.emails(), strategies.text()) +@settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) +def test_credentials_not_in_database(conn_after_auth_migrations, email, password): + """ + GIVEN: credentials that do not exist in the database + WHEN: the `credentials_in_database` function is run against the credentials + THEN: check that the function returns false in all cases. + """ + with closing(conn_after_auth_migrations.cursor()) as cursor: + results = credentials_in_database(cursor, email, password) + assert credentials_in_database(cursor, email, password) is False diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index bc974d9..dd6abb4 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,11 +1,13 @@ """Fixtures for unit tests.""" +import sqlite3 from typing import Union from pathlib import Path from datetime import datetime +from contextlib import closing from tempfile import TemporaryDirectory import pytest -from yoyo import get_backend +from yoyo import get_backend, read_migrations from yoyo.migrations import Migration, MigrationList from gn3.app import create_app @@ -25,16 +27,21 @@ def client(): # Clean up after ourselves testdb.unlink(missing_ok=True) -@pytest.fixture() +@pytest.fixture(scope="session") def test_app_config(client): # pylint: disable=redefined-outer-name """Return the test application's configuration object""" return client.application.config -@pytest.fixture() -def auth_testdb(test_app_config): # pylint: disable=redefined-outer-name +@pytest.fixture(scope="session") +def auth_testdb_path(test_app_config): # pylint: disable=redefined-outer-name """Get the test application's auth database file""" return test_app_config["AUTH_DB"] +@pytest.fixture(scope="session") +def auth_migrations_dir(test_app_config): # pylint: disable=redefined-outer-name + """Get the test application's auth database file""" + return test_app_config["AUTH_MIGRATIONS"] + def apply_single_migration(db_uri: Union[Path, str], migration: Migration): """Utility to apply a single migration""" apply_migrations(get_backend(f"sqlite:///{db_uri}"), MigrationList([migration])) @@ -42,3 +49,14 @@ def apply_single_migration(db_uri: Union[Path, str], migration: Migration): def rollback_single_migration(db_uri: Union[Path, str], migration: Migration): """Utility to rollback a single migration""" rollback_migrations(get_backend(f"sqlite:///{db_uri}"), MigrationList([migration])) + +@pytest.fixture(scope="function") +def conn_after_auth_migrations(auth_testdb_path, auth_migrations_dir): + """Run all migrations and return a connection to the database after""" + backend = get_backend(f"sqlite:///{auth_testdb_path}") + migrations = read_migrations(auth_migrations_dir) + apply_migrations(backend, migrations) + with closing(sqlite3.connect(auth_testdb_path)) as conn: + yield conn + + rollback_migrations(backend, migrations) -- cgit v1.2.3