about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--gn_auth/__init__.py65
-rw-r--r--gn_auth/auth/authentication/users.py4
-rw-r--r--gn_auth/auth/authorisation/checks.py2
-rw-r--r--gn_auth/auth/authorisation/privileges.py4
-rw-r--r--gn_auth/migrations.py33
-rw-r--r--gn_auth/settings.py18
-rw-r--r--main.py129
-rw-r--r--setup.py4
9 files changed, 255 insertions, 7 deletions
diff --git a/.gitignore b/.gitignore
index d64ca2c..0767c74 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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")
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..4a4850c
--- /dev/null
+++ b/main.py
@@ -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()
diff --git a/setup.py b/setup.py
index 52a928e..31fd889 100644
--- a/setup.py
+++ b/setup.py
@@ -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=[