diff options
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | gn_auth/__init__.py | 65 | ||||
-rw-r--r-- | gn_auth/auth/authentication/users.py | 4 | ||||
-rw-r--r-- | gn_auth/auth/authorisation/checks.py | 2 | ||||
-rw-r--r-- | gn_auth/auth/authorisation/privileges.py | 4 | ||||
-rw-r--r-- | gn_auth/migrations.py | 33 | ||||
-rw-r--r-- | gn_auth/settings.py | 18 | ||||
-rw-r--r-- | main.py | 129 | ||||
-rw-r--r-- | setup.py | 4 |
9 files changed, 255 insertions, 7 deletions
@@ -1,3 +1,6 @@ +# python temp files and cache +/**/__pycache__ + # emacs temporary files /**/*~ diff --git a/gn_auth/__init__.py b/gn_auth/__init__.py index e69de29..4ef2892 100644 --- a/gn_auth/__init__.py +++ b/gn_auth/__init__.py @@ -0,0 +1,65 @@ +import os +import sys +import logging + +from flask import Flask + +from . import settings + +class ConfigurationError(Exception): + """Raised in case of a configuration error.""" + +def __check_secret_key__(app: Flask) -> None: + """Verify secret key is not empty.""" + if app.config.get("SECRET_KEY", "") == "": + raise ConfigurationError("The `SECRET_KEY` settings cannot be empty.") + +def check_mandatory_settings(app: Flask) -> None: + """Verify that mandatory settings are defined in the application""" + undefined = tuple( + setting for setting in ( + "SECRET_KEY", "SQL_URI", "AUTH_DB", "AUTH_MIGRATIONS", + "OAUTH2_SCOPE") + if setting not in app.config) + if len(undefined) > 0: + raise ConfigurationError( + "You must provide values for the following settings: " + + "\t* " + "\n\t* ".join(undefined)) + + __check_secret_key__(app) + +def override_settings_with_envvars( + app: Flask, ignore: tuple[str, ...]=tuple()) -> None: + """Override settings in `app` with those in ENVVARS""" + for setting in (key for key in app.config if key not in ignore): + app.config[setting] = os.environ.get(setting) or app.config[setting] + +def setup_logging_handlers(app: Flask) -> None: + """Setup the loggging handlers.""" + stderr_handler = logging.StreamHandler(stream=sys.stderr) + app.logger.addHandler(stderr_handler) + + root_logger = logging.getLogger() + root_logger.addHandler(stderr_handler) + root_logger.setLevel(app.config["LOGLEVEL"]) + +def create_app(config: dict = {}) -> Flask: + """Create and return a new flask application.""" + app = Flask(__name__) + + # ====== Setup configuration ====== + app.config.from_object(settings) # Default settings + # Override defaults with startup settings + app.config.update(config) + # Override app settings with site-local settings + if "GN_AUTH_CONF" in os.environ: + app.config.from_envvar("GN_AUTH_CONF") + + override_settings_with_envvars(app) + # ====== END: Setup configuration ====== + + check_mandatory_settings(app) + + setup_logging_handlers(app) + + return app diff --git a/gn_auth/auth/authentication/users.py b/gn_auth/auth/authentication/users.py index 0e72ed2..327820e 100644 --- a/gn_auth/auth/authentication/users.py +++ b/gn_auth/auth/authentication/users.py @@ -5,8 +5,8 @@ from typing import Any, Tuple, NamedTuple from argon2 import PasswordHasher from argon2.exceptions import VerifyMismatchError -from gn3.auth import db -from gn3.auth.authorisation.errors import NotFoundError +from gn_auth.auth import db +from gn_auth.auth.authorisation.errors import NotFoundError class User(NamedTuple): """Class representing a user.""" diff --git a/gn_auth/auth/authorisation/checks.py b/gn_auth/auth/authorisation/checks.py index 1c87c02..02c6810 100644 --- a/gn_auth/auth/authorisation/checks.py +++ b/gn_auth/auth/authorisation/checks.py @@ -4,7 +4,7 @@ from typing import Callable from flask import request, current_app as app -from gn3.auth import db +from gn_auth.auth import db from . import privileges as auth_privs from .errors import InvalidData, AuthorisationError diff --git a/gn_auth/auth/authorisation/privileges.py b/gn_auth/auth/authorisation/privileges.py index dbb4129..1b0f06c 100644 --- a/gn_auth/auth/authorisation/privileges.py +++ b/gn_auth/auth/authorisation/privileges.py @@ -1,8 +1,8 @@ """Handle privileges""" from typing import Any, Iterable, NamedTuple -from gn3.auth import db -from gn3.auth.authentication.users import User +from gn_auth.auth import db +from gn_auth.auth.authentication.users import User class Privilege(NamedTuple): """Class representing a privilege: creates immutable objects.""" diff --git a/gn_auth/migrations.py b/gn_auth/migrations.py new file mode 100644 index 0000000..3451e07 --- /dev/null +++ b/gn_auth/migrations.py @@ -0,0 +1,33 @@ +"""Run the migrations in the app, rather than with yoyo CLI.""" +from pathlib import Path +from typing import Union + +from yoyo import read_migrations +from yoyo.backends import DatabaseBackend +from yoyo.migrations import Migration, MigrationList + +class MigrationNotFound(Exception): + """Raised if a migration is not found at the given path.""" + def __init__(self, migration_path: Path): + """Initialise the exception.""" + super().__init__(f"Could not find migration '{migration_path}'") + +def apply_migrations(backend: DatabaseBackend, migrations: MigrationList): + "Apply the provided migrations." + with backend.lock(): + backend.apply_migrations(backend.to_apply(migrations)) + +def rollback_migrations(backend: DatabaseBackend, migrations: MigrationList): + "Rollback the provided migrations." + with backend.lock(): + backend.rollback_migrations(backend.to_rollback(migrations)) + +def get_migration(migration_path: Union[Path, str]) -> Migration: + """Retrieve a migration at thi given `migration_path`.""" + migration_path = Path(migration_path) + if migration_path.exists(): + for migration in read_migrations(str(migration_path.parent)): + if Path(migration.path) == migration_path: + return migration + + raise MigrationNotFound(migration_path) diff --git a/gn_auth/settings.py b/gn_auth/settings.py new file mode 100644 index 0000000..71ffd5d --- /dev/null +++ b/gn_auth/settings.py @@ -0,0 +1,18 @@ +"""Default application settings.""" +import os + +# LOGLEVEL +LOGLEVEL = "WARNING" + +# Flask settings +SECRET_KEY = "" + +# Database settings +SQL_URI = "mysql://webqtlout:webqtlout@localhost/db_webqtl" +AUTH_DB = f"{os.environ.get('HOME')}/genenetwork/gn3_files/db/auth.db" +AUTH_MIGRATIONS = "migrations/auth" + +# OAuth2 settings +OAUTH2_SCOPE = ( + "profile", "group", "role", "resource", "user", "masquerade", + "introspect") @@ -0,0 +1,129 @@ +"""Main entry point for project""" +import sys +import uuid +import json +from math import ceil +from pathlib import Path +from datetime import datetime + + +import click +from yoyo import get_backend, read_migrations + +from gn_auth import migrations +from gn_auth import create_app + +from gn_auth.auth import db +from gn_auth.auth.authentication.users import hash_password + +from scripts import register_sys_admin as rsysadm# type: ignore[import] +from scripts import migrate_existing_data as med# type: ignore[import] + +app = create_app() + +##### BEGIN: CLI Commands ##### + +@app.cli.command() +def apply_migrations(): + """Apply the dabasase migrations.""" + migrations.apply_migrations( + get_backend(f'sqlite:///{app.config["AUTH_DB"]}'), + read_migrations(app.config["AUTH_MIGRATIONS"])) + +def __init_dev_users__(): + """Initialise dev users. Get's used in more than one place""" + dev_users_query = "INSERT INTO users VALUES (:user_id, :email, :name)" + dev_users_passwd = "INSERT INTO user_credentials VALUES (:user_id, :hash)" + dev_users = ({ + "user_id": "0ad1917c-57da-46dc-b79e-c81c91e5b928", + "email": "test@development.user", + "name": "Test Development User", + "password": "testpasswd"},) + + with db.connection(app.config["AUTH_DB"]) as conn, db.cursor(conn) as cursor: + cursor.executemany(dev_users_query, dev_users) + cursor.executemany(dev_users_passwd, ( + {**usr, "hash": hash_password(usr["password"])} + for usr in dev_users)) + +@app.cli.command() +def init_dev_users(): + """ + Initialise development users for OAuth2 sessions. + + **NOTE**: You really should not run this in production/staging + """ + __init_dev_users__() + +@app.cli.command() +def init_dev_clients(): + """ + Initialise a development client for OAuth2 sessions. + + **NOTE**: You really should not run this in production/staging + """ + __init_dev_users__() + dev_clients_query = ( + "INSERT INTO oauth2_clients VALUES (" + ":client_id, :client_secret, :client_id_issued_at, " + ":client_secret_expires_at, :client_metadata, :user_id" + ")") + dev_clients = ({ + "client_id": "0bbfca82-d73f-4bd4-a140-5ae7abb4a64d", + "client_secret": "yadabadaboo", + "client_id_issued_at": ceil(datetime.now().timestamp()), + "client_secret_expires_at": 0, + "client_metadata": json.dumps({ + "client_name": "GN2 Dev Server", + "token_endpoint_auth_method": [ + "client_secret_post", "client_secret_basic"], + "client_type": "confidential", + "grant_types": ["password", "authorization_code", "refresh_token"], + "default_redirect_uri": "http://localhost:5033/oauth2/code", + "redirect_uris": ["http://localhost:5033/oauth2/code", + "http://localhost:5033/oauth2/token"], + "response_type": ["code", "token"], + "scope": ["profile", "group", "role", "resource", "register-client", + "user", "masquerade", "migrate-data", "introspect"] + }), + "user_id": "0ad1917c-57da-46dc-b79e-c81c91e5b928"},) + + with db.connection(app.config["AUTH_DB"]) as conn, db.cursor(conn) as cursor: + cursor.executemany(dev_clients_query, dev_clients) + + +@app.cli.command() +@click.argument("user_id", type=click.UUID) +def assign_system_admin(user_id: uuid.UUID): + """Assign user with ID `user_id` administrator role.""" + dburi = app.config["AUTH_DB"] + with db.connection(dburi) as conn, db.cursor(conn) as cursor: + cursor.execute("SELECT * FROM users WHERE user_id=?", + (str(user_id),)) + row = cursor.fetchone() + if row: + cursor.execute( + "SELECT * FROM roles WHERE role_name='system-administrator'") + admin_role = cursor.fetchone() + cursor.execute("INSERT INTO user_roles VALUES (?,?)", + (str(user_id), admin_role["role_id"])) + return 0 + print(f"ERROR: Could not find user with ID {user_id}", + file=sys.stderr) + sys.exit(1) + +@app.cli.command() +def make_data_public(): + """Make existing data that is not assigned to any group publicly visible.""" + med.entry(app.config["AUTH_DB"], app.config["SQL_URI"]) + +@app.cli.command() +def register_admin(): + """Register the administrator.""" + rsysadm.register_admin(Path(app.config["AUTH_DB"])) + +##### END: CLI Commands ##### + +if __name__ == '__main__': + print("Starting app...") + app.run() @@ -3,7 +3,7 @@ from setuptools import setup from setup_commands import RunTests -long_description = """ +LONG_DESCRIPTION = """ GeneNetwork-Auth project is the authentication/authorisation server to be used across all GeneNetwork services. """ @@ -28,7 +28,7 @@ setup(author="Frederick M. Muriithi", ], scripts=[], license="AGPLV3", - long_description=long_description, + long_description=LONG_DESCRIPTION, long_description_content_type="text/markdown", name="GeneNetwork-Auth", packages=[ |