diff options
105 files changed, 35 insertions, 4272 deletions
@@ -27,7 +27,6 @@ from gn3.api.search import search from gn3.api.metadata import metadata from gn3.api.sampledata import sampledata from gn3.auth import oauth2 -from gn3.auth.authentication.oauth2.server import setup_oauth2_server def create_app(config: Union[Dict, str, None] = None) -> Flask: @@ -79,5 +78,4 @@ def create_app(config: Union[Dict, str, None] = None) -> Flask: app.register_blueprint(oauth2, url_prefix="/api/oauth2") register_error_handlers(app) - setup_oauth2_server(app) return app diff --git a/gn3/auth/__init__.py b/gn3/auth/__init__.py index a28498d..cd65e9b 100644 --- a/gn3/auth/__init__.py +++ b/gn3/auth/__init__.py @@ -1,5 +1,4 @@ """Top-Level `Auth` module""" from . import authorisation -from . import authentication from .views import oauth2 diff --git a/gn3/auth/authentication/__init__.py b/gn3/auth/authentication/__init__.py deleted file mode 100644 index 42ceacb..0000000 --- a/gn3/auth/authentication/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -"""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.user_id = user_credentials.user_id " - "WHERE users.email = :email"), - {"email": email}) - results = cursor.fetchall() - if len(results) == 0: - return False - - assert len(results) == 1, "Expected one row." - row = results[0] - return (email == row[0] and - bcrypt.checkpw(password.encode("utf-8"), row[1])) diff --git a/gn3/auth/authentication/exceptions.py b/gn3/auth/authentication/exceptions.py deleted file mode 100644 index c31e691..0000000 --- a/gn3/auth/authentication/exceptions.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Exceptions for authentication""" - -class AuthenticationError(Exception): - """Base exception class for `gn3.auth.authentication` package.""" diff --git a/gn3/auth/authentication/oauth2/__init__.py b/gn3/auth/authentication/oauth2/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/gn3/auth/authentication/oauth2/__init__.py +++ /dev/null diff --git a/gn3/auth/authentication/oauth2/endpoints/__init__.py b/gn3/auth/authentication/oauth2/endpoints/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/gn3/auth/authentication/oauth2/endpoints/__init__.py +++ /dev/null diff --git a/gn3/auth/authentication/oauth2/endpoints/introspection.py b/gn3/auth/authentication/oauth2/endpoints/introspection.py deleted file mode 100644 index cfe2998..0000000 --- a/gn3/auth/authentication/oauth2/endpoints/introspection.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Handle introspection of tokens.""" -import datetime -from urllib.parse import urlparse - -from flask import request as flask_request -from authlib.oauth2.rfc7662 import ( - IntrospectionEndpoint as _IntrospectionEndpoint) - -from gn3.auth.authentication.oauth2.models.oauth2token import OAuth2Token - -from .utilities import query_token as _query_token - -def get_token_user_sub(token: OAuth2Token) -> str:# pylint: disable=[unused-argument] - """ - Return the token's subject as defined in - https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2 - """ - ## For now a dummy return to prevent issues. - return "sub" - -class IntrospectionEndpoint(_IntrospectionEndpoint): - """Introspect token.""" - def query_token(self, token_string: str, token_type_hint: str): - """Query the token.""" - return _query_token(self, token_string, token_type_hint) - - def introspect_token(self, token: OAuth2Token) -> dict: - """Return the introspection information.""" - url = urlparse(flask_request.url) - return { - "active": True, - "scope": token.get_scope(), - "client_id": token.client.client_id, - "username": token.user.name, - "token_type": token.token_type, - "exp": int(token.expires_at.timestamp()), - "iat": int(token.issued_at.timestamp()), - "nbf": int( - (token.issued_at - datetime.timedelta(seconds=120)).timestamp()), - # "sub": get_token_user_sub(token), - "aud": token.client.client_id, - "iss": f"{url.scheme}://{url.netloc}", - "jti": token.token_id - } - - def check_permission(self, token, client, request): - """Check that the client has permission to introspect token.""" - return client.client_type == "internal" diff --git a/gn3/auth/authentication/oauth2/endpoints/revocation.py b/gn3/auth/authentication/oauth2/endpoints/revocation.py deleted file mode 100644 index b8517b6..0000000 --- a/gn3/auth/authentication/oauth2/endpoints/revocation.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Handle token revocation.""" - -from flask import current_app -from authlib.oauth2.rfc7009 import RevocationEndpoint as _RevocationEndpoint - -from gn3.auth import db -from gn3.auth.authentication.oauth2.models.oauth2token import ( - save_token, OAuth2Token, revoke_token) - -from .utilities import query_token as _query_token - -class RevocationEndpoint(_RevocationEndpoint): - """Revoke the tokens""" - ENDPOINT_NAME = "revoke" - def query_token(self, token_string: str, token_type_hint: str): - """Query the token.""" - return _query_token(self, token_string, token_type_hint) - - def revoke_token(self, token: OAuth2Token, request): - """Revoke token `token`.""" - with db.connection(current_app.config["AUTH_DB"]) as conn: - save_token(conn, revoke_token(token)) diff --git a/gn3/auth/authentication/oauth2/endpoints/utilities.py b/gn3/auth/authentication/oauth2/endpoints/utilities.py deleted file mode 100644 index e13784e..0000000 --- a/gn3/auth/authentication/oauth2/endpoints/utilities.py +++ /dev/null @@ -1,31 +0,0 @@ -"""endpoint utilities""" -from typing import Any, Optional - -from flask import current_app -from pymonad.maybe import Nothing - -from gn3.auth import db -from gn3.auth.authentication.oauth2.models.oauth2token import ( - OAuth2Token, token_by_access_token, token_by_refresh_token) - -def query_token(# pylint: disable=[unused-argument] - endpoint_object: Any, token_str: str, token_type_hint) -> Optional[ - OAuth2Token]: - """Retrieve the token from the database.""" - def __identity__(val): - return val - token = Nothing - with db.connection(current_app.config["AUTH_DB"]) as conn: - if token_type_hint == "access_token": - token = token_by_access_token(conn, token_str) - if token_type_hint == "access_token": - token = token_by_refresh_token(conn, token_str) - - return token.maybe( - token_by_access_token(conn, token_str).maybe( - token_by_refresh_token(conn, token_str).maybe( - None, __identity__), - __identity__), - __identity__) - - return None diff --git a/gn3/auth/authentication/oauth2/grants/__init__.py b/gn3/auth/authentication/oauth2/grants/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/gn3/auth/authentication/oauth2/grants/__init__.py +++ /dev/null diff --git a/gn3/auth/authentication/oauth2/grants/authorisation_code_grant.py b/gn3/auth/authentication/oauth2/grants/authorisation_code_grant.py deleted file mode 100644 index fb8d436..0000000 --- a/gn3/auth/authentication/oauth2/grants/authorisation_code_grant.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Classes and function for Authorisation Code flow.""" -import uuid -import string -import random -from typing import Optional -from datetime import datetime - -from flask import current_app as app -from authlib.oauth2.rfc6749 import grants -from authlib.oauth2.rfc7636 import create_s256_code_challenge - -from gn3.auth import db -from gn3.auth.db_utils import with_db_connection -from gn3.auth.authentication.users import User - -from ..models.oauth2client import OAuth2Client -from ..models.authorization_code import ( - AuthorisationCode, authorisation_code, save_authorisation_code) - -class AuthorisationCodeGrant(grants.AuthorizationCodeGrant): - """Implement the 'Authorisation Code' grant.""" - TOKEN_ENDPOINT_AUTH_METHODS: list[str] = [ - "client_secret_basic", "client_secret_post"] - AUTHORIZATION_CODE_LENGTH: int = 48 - TOKEN_ENDPOINT_HTTP_METHODS = ['POST'] - GRANT_TYPE = "authorization_code" - RESPONSE_TYPES = {'code'} - - def save_authorization_code(self, code, request): - """Persist the authorisation code to database.""" - client = request.client - nonce = "".join(random.sample(string.ascii_letters + string.digits, - k=self.AUTHORIZATION_CODE_LENGTH)) - return __save_authorization_code__(AuthorisationCode( - uuid.uuid4(), code, client, request.redirect_uri, request.scope, - nonce, int(datetime.now().timestamp()), - create_s256_code_challenge(app.config["SECRET_KEY"]), - "S256", request.user)) - - def query_authorization_code(self, code, client): - """Retrieve the code from the database.""" - return __query_authorization_code__(code, client) - - def delete_authorization_code(self, authorization_code): - """Delete the authorisation code.""" - with db.connection(app.config["AUTH_DB"]) as conn: - with db.cursor(conn) as cursor: - cursor.execute( - "DELETE FROM authorisation_code WHERE code_id=?", - (str(authorization_code.code_id),)) - - def authenticate_user(self, authorization_code) -> Optional[User]: - """Authenticate the user who own the authorisation code.""" - query = ( - "SELECT users.* FROM authorisation_code LEFT JOIN users " - "ON authorisation_code.user_id=users.user_id " - "WHERE authorisation_code.code=?") - with db.connection(app.config["AUTH_DB"]) as conn: - with db.cursor(conn) as cursor: - cursor.execute(query, (str(authorization_code.code),)) - res = cursor.fetchone() - if res: - return User( - uuid.UUID(res["user_id"]), res["email"], res["name"]) - - return None - -def __query_authorization_code__( - code: str, client: OAuth2Client) -> AuthorisationCode: - """A helper function that creates a new database connection. - - This is found to be necessary since the `AuthorizationCodeGrant` class(es) - do not have a way to pass the database connection.""" - def __auth_code__(conn) -> str: - the_code = authorisation_code(conn, code, client) - return the_code.maybe(None, lambda cde: cde) # type: ignore[misc, arg-type, return-value] - - return with_db_connection(__auth_code__) - -def __save_authorization_code__(code: AuthorisationCode) -> AuthorisationCode: - """A helper function that creates a new database connection. - - This is found to be necessary since the `AuthorizationCodeGrant` class(es) - do not have a way to pass the database connection.""" - return with_db_connection(lambda conn: save_authorisation_code(conn, code)) diff --git a/gn3/auth/authentication/oauth2/grants/password_grant.py b/gn3/auth/authentication/oauth2/grants/password_grant.py deleted file mode 100644 index 3233877..0000000 --- a/gn3/auth/authentication/oauth2/grants/password_grant.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Allows users to authenticate directly.""" - -from flask import current_app as app -from authlib.oauth2.rfc6749 import grants - -from gn3.auth import db -from gn3.auth.authentication.users import valid_login, user_by_email - -from gn3.auth.authorisation.errors import NotFoundError - -class PasswordGrant(grants.ResourceOwnerPasswordCredentialsGrant): - """Implement the 'Password' grant.""" - TOKEN_ENDPOINT_AUTH_METHODS = ["client_secret_basic", "client_secret_post"] - - def authenticate_user(self, username, password): - "Authenticate the user with their username and password." - with db.connection(app.config["AUTH_DB"]) as conn: - try: - user = user_by_email(conn, username) - return user if valid_login(conn, user, password) else None - except NotFoundError as _nfe: - return None diff --git a/gn3/auth/authentication/oauth2/models/__init__.py b/gn3/auth/authentication/oauth2/models/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/gn3/auth/authentication/oauth2/models/__init__.py +++ /dev/null diff --git a/gn3/auth/authentication/oauth2/models/authorization_code.py b/gn3/auth/authentication/oauth2/models/authorization_code.py deleted file mode 100644 index f282814..0000000 --- a/gn3/auth/authentication/oauth2/models/authorization_code.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Model and functions for handling the Authorisation Code""" -from uuid import UUID -from datetime import datetime -from typing import NamedTuple - -from pymonad.maybe import Just, Maybe, Nothing - -from gn3.auth import db - -from .oauth2client import OAuth2Client - -from ...users import User, user_by_id - -__5_MINUTES__ = 300 # in seconds - -class AuthorisationCode(NamedTuple): - """ - The AuthorisationCode model for the auth(entic|oris)ation system. - """ - # Instance variables - code_id: UUID - code: str - client: OAuth2Client - redirect_uri: str - scope: str - nonce: str - auth_time: int - code_challenge: str - code_challenge_method: str - user: User - - @property - def response_type(self) -> str: - """ - For authorisation code flow, the response_type type MUST always be - 'code'. - """ - return "code" - - def is_expired(self): - """Check whether the code is expired.""" - return self.auth_time + __5_MINUTES__ < datetime.now().timestamp() - - def get_redirect_uri(self): - """Get the redirect URI""" - return self.redirect_uri - - def get_scope(self): - """Return the assigned scope for this AuthorisationCode.""" - return self.scope - - def get_nonce(self): - """Get the one-time use token.""" - return self.nonce - -def authorisation_code(conn: db.DbConnection , - code: str, - client: OAuth2Client) -> Maybe[AuthorisationCode]: - """ - Retrieve the authorisation code object that corresponds to `code` and the - given OAuth2 client. - """ - with db.cursor(conn) as cursor: - query = ("SELECT * FROM authorisation_code " - "WHERE code=:code AND client_id=:client_id") - cursor.execute( - query, {"code": code, "client_id": str(client.client_id)}) - result = cursor.fetchone() - if result: - return Just(AuthorisationCode( - UUID(result["code_id"]), result["code"], client, - result["redirect_uri"], result["scope"], result["nonce"], - int(result["auth_time"]), result["code_challenge"], - result["code_challenge_method"], - user_by_id(conn, UUID(result["user_id"])))) - return Nothing - -def save_authorisation_code(conn: db.DbConnection, - auth_code: AuthorisationCode) -> AuthorisationCode: - """Persist the `auth_code` into the database.""" - with db.cursor(conn) as cursor: - cursor.execute( - "INSERT INTO authorisation_code VALUES(" - ":code_id, :code, :client_id, :redirect_uri, :scope, :nonce, " - ":auth_time, :code_challenge, :code_challenge_method, :user_id" - ")", - { - **auth_code._asdict(), - "code_id": str(auth_code.code_id), - "client_id": str(auth_code.client.client_id), - "user_id": str(auth_code.user.user_id) - }) - return auth_code diff --git a/gn3/auth/authentication/oauth2/server.py b/gn3/auth/authentication/oauth2/server.py deleted file mode 100644 index 7d7113a..0000000 --- a/gn3/auth/authentication/oauth2/server.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Initialise the OAuth2 Server""" -import uuid -import datetime -from typing import Callable - -from flask import Flask, current_app -from authlib.oauth2.rfc6749.errors import InvalidClientError -from authlib.integrations.flask_oauth2 import AuthorizationServer -# from authlib.oauth2.rfc7636 import CodeChallenge - -from gn3.auth import db - -from .models.oauth2client import client -from .models.oauth2token import OAuth2Token, save_token - -from .grants.password_grant import PasswordGrant -from .grants.authorisation_code_grant import AuthorisationCodeGrant - -from .endpoints.revocation import RevocationEndpoint -from .endpoints.introspection import IntrospectionEndpoint - -def create_query_client_func() -> Callable: - """Create the function that loads the client.""" - def __query_client__(client_id: uuid.UUID): - # use current_app rather than passing the db_uri to avoid issues - # when config changes, e.g. while testing. - with db.connection(current_app.config["AUTH_DB"]) as conn: - the_client = client(conn, client_id).maybe( - None, lambda clt: clt) # type: ignore[misc] - if bool(the_client): - return the_client - raise InvalidClientError( - "No client found for the given CLIENT_ID and CLIENT_SECRET.") - - return __query_client__ - -def create_save_token_func(token_model: type) -> Callable: - """Create the function that saves the token.""" - def __save_token__(token, request): - with db.connection(current_app.config["AUTH_DB"]) as conn: - save_token( - conn, token_model( - token_id=uuid.uuid4(), client=request.client, - user=request.user, - **{ - "refresh_token": None, "revoked": False, - "issued_at": datetime.datetime.now(), - **token - })) - - return __save_token__ - -def setup_oauth2_server(app: Flask) -> None: - """Set's up the oauth2 server for the flask application.""" - server = AuthorizationServer() - server.register_grant(PasswordGrant) - - # Figure out a common `code_verifier` for GN2 and GN3 and set - # server.register_grant(AuthorisationCodeGrant, [CodeChallenge(required=False)]) - # below - server.register_grant(AuthorisationCodeGrant) - - # register endpoints - server.register_endpoint(RevocationEndpoint) - server.register_endpoint(IntrospectionEndpoint) - - # init server - server.init_app( - app, - query_client=create_query_client_func(), - save_token=create_save_token_func(OAuth2Token)) - app.config["OAUTH2_SERVER"] = server diff --git a/gn3/auth/authentication/oauth2/views.py b/gn3/auth/authentication/oauth2/views.py deleted file mode 100644 index 2bd3865..0000000 --- a/gn3/auth/authentication/oauth2/views.py +++ /dev/null @@ -1,104 +0,0 @@ -"""Endpoints for the oauth2 server""" -import uuid -import traceback - -from authlib.oauth2.rfc6749.errors import InvalidClientError -from email_validator import validate_email, EmailNotValidError -from flask import ( - flash, - request, - url_for, - redirect, - Response, - Blueprint, - render_template, - current_app as app) - -from gn3.auth import db -from gn3.auth.db_utils import with_db_connection -from gn3.auth.authorisation.errors import ForbiddenAccess - -from .resource_server import require_oauth -from .endpoints.revocation import RevocationEndpoint -from .endpoints.introspection import IntrospectionEndpoint - -from ..users import valid_login, NotFoundError, user_by_email - -auth = Blueprint("auth", __name__) - -@auth.route("/delete-client/<uuid:client_id>", methods=["GET", "POST"]) -def delete_client(client_id: uuid.UUID): - """Delete an OAuth2 client.""" - return f"WOULD DELETE OAUTH2 CLIENT {client_id}." - -@auth.route("/authorise", methods=["GET", "POST"]) -def authorise(): - """Authorise a user""" - try: - server = app.config["OAUTH2_SERVER"] - client_id = uuid.UUID(request.args.get( - "client_id", - request.form.get("client_id", str(uuid.uuid4())))) - client = server.query_client(client_id) - if not bool(client): - flash("Invalid OAuth2 client.", "alert-error") - if request.method == "GET": - client = server.query_client(request.args.get("client_id")) - return render_template( - "oauth2/authorise-user.html", - client=client, - scope=client.scope, - response_type="code") - - form = request.form - def __authorise__(conn: db.DbConnection) -> Response: - email_passwd_msg = "Email or password is invalid!" - redirect_response = redirect(url_for("oauth2.auth.authorise", - client_id=client_id)) - try: - email = validate_email( - form.get("user:email"), check_deliverability=False) - user = user_by_email(conn, email["email"]) - if valid_login(conn, user, form.get("user:password", "")): - return server.create_authorization_response(request=request, grant_user=user) - flash(email_passwd_msg, "alert-error") - return redirect_response # type: ignore[return-value] - except EmailNotValidError as _enve: - app.logger.debug(traceback.format_exc()) - flash(email_passwd_msg, "alert-error") - return redirect_response # type: ignore[return-value] - except NotFoundError as _nfe: - app.logger.debug(traceback.format_exc()) - flash(email_passwd_msg, "alert-error") - return redirect_response # type: ignore[return-value] - - return with_db_connection(__authorise__) - except InvalidClientError as ice: - return render_template( - "oauth2/oauth2_error.html", error=ice), ice.status_code - -@auth.route("/token", methods=["POST"]) -def token(): - """Retrieve the authorisation token.""" - server = app.config["OAUTH2_SERVER"] - return server.create_token_response() - -@auth.route("/revoke", methods=["POST"]) -def revoke_token(): - """Revoke the token.""" - return app.config["OAUTH2_SERVER"].create_endpoint_response( - RevocationEndpoint.ENDPOINT_NAME) - -@auth.route("/introspect", methods=["POST"]) -@require_oauth("introspect") -def introspect_token() -> Response: - """Provide introspection information for the token.""" - # This is dangerous to provide publicly - authorised_clients = app.config.get( - "OAUTH2_CLIENTS_WITH_INTROSPECTION_PRIVILEGE", []) - with require_oauth.acquire("introspect") as the_token: - if the_token.client.client_id in authorised_clients: - return app.config["OAUTH2_SERVER"].create_endpoint_response( - IntrospectionEndpoint.ENDPOINT_NAME) - - raise ForbiddenAccess("You cannot access this endpoint") diff --git a/gn3/auth/authorisation/checks.py b/gn3/auth/authorisation/checks.py index 1c87c02..17daca4 100644 --- a/gn3/auth/authorisation/checks.py +++ b/gn3/auth/authorisation/checks.py @@ -5,12 +5,11 @@ from typing import Callable from flask import request, current_app as app from gn3.auth import db +from gn3.auth.authorisation.oauth2.resource_server import require_oauth from . import privileges as auth_privs from .errors import InvalidData, AuthorisationError -from ..authentication.oauth2.resource_server import require_oauth - def __system_privileges_in_roles__(conn, user): """ This really is a hack since groups are not treated as resources at the diff --git a/gn3/auth/authorisation/data/views.py b/gn3/auth/authorisation/data/views.py index 8adf862..81811dd 100644 --- a/gn3/auth/authorisation/data/views.py +++ b/gn3/auth/authorisation/data/views.py @@ -29,9 +29,9 @@ from gn3.auth.authorisation.resources.checks import authorised_for from gn3.auth.authorisation.resources.models import ( user_resources, public_resources, attach_resources_data) -from gn3.auth.authentication.users import User -from gn3.auth.authentication.oauth2.resource_server import require_oauth +from gn3.auth.authorisation.oauth2.resource_server import require_oauth +from gn3.auth.authorisation.users import User from gn3.auth.authorisation.data.phenotypes import link_phenotype_data from gn3.auth.authorisation.data.mrna import link_mrna_data, ungrouped_mrna_data from gn3.auth.authorisation.data.genotypes import ( diff --git a/gn3/auth/authorisation/groups/models.py b/gn3/auth/authorisation/groups/models.py index 5a3ae50..7212a78 100644 --- a/gn3/auth/authorisation/groups/models.py +++ b/gn3/auth/authorisation/groups/models.py @@ -9,7 +9,7 @@ from pymonad.maybe import Just, Maybe, Nothing from gn3.auth import db from gn3.auth.dictify import dictify -from gn3.auth.authentication.users import User, user_by_id +from gn3.auth.authorisation.users import User, user_by_id from ..checks import authorised_p from ..privileges import Privilege diff --git a/gn3/auth/authorisation/groups/views.py b/gn3/auth/authorisation/groups/views.py index 628df36..a849a73 100644 --- a/gn3/auth/authorisation/groups/views.py +++ b/gn3/auth/authorisation/groups/views.py @@ -12,6 +12,8 @@ from gn3 import db_utils as gn3db from gn3.auth.dictify import dictify from gn3.auth.db_utils import with_db_connection +from gn3.auth.authorisation.users import User +from gn3.auth.authorisation.oauth2.resource_server import require_oauth from .data import link_data_to_group from .models import ( @@ -28,9 +30,6 @@ from ..checks import authorised_p from ..privileges import Privilege, privileges_by_ids from ..errors import InvalidData, NotFoundError, AuthorisationError -from ...authentication.users import User -from ...authentication.oauth2.resource_server import require_oauth - groups = Blueprint("groups", __name__) @groups.route("/list", methods=["GET"]) diff --git a/gn3/auth/authentication/oauth2/models/oauth2client.py b/gn3/auth/authorisation/oauth2/oauth2client.py index 2a307e3..dc54a41 100644 --- a/gn3/auth/authentication/oauth2/models/oauth2client.py +++ b/gn3/auth/authorisation/oauth2/oauth2client.py @@ -7,9 +7,9 @@ from typing import Sequence, Optional, NamedTuple from pymonad.maybe import Just, Maybe, Nothing from gn3.auth import db -from gn3.auth.authentication.users import User, users, user_by_id, same_password from gn3.auth.authorisation.errors import NotFoundError +from gn3.auth.authorisation.users import User, users, user_by_id, same_password class OAuth2Client(NamedTuple): """ diff --git a/gn3/auth/authentication/oauth2/models/oauth2token.py b/gn3/auth/authorisation/oauth2/oauth2token.py index bfe4aaf..bb19039 100644 --- a/gn3/auth/authentication/oauth2/models/oauth2token.py +++ b/gn3/auth/authorisation/oauth2/oauth2token.py @@ -6,9 +6,9 @@ from typing import NamedTuple, Optional from pymonad.maybe import Just, Maybe, Nothing from gn3.auth import db -from gn3.auth.authentication.users import User, user_by_id from gn3.auth.authorisation.errors import NotFoundError +from gn3.auth.authorisation.users import User, user_by_id from .oauth2client import client, OAuth2Client diff --git a/gn3/auth/authentication/oauth2/resource_server.py b/gn3/auth/authorisation/oauth2/resource_server.py index 223e811..e806dc5 100644 --- a/gn3/auth/authentication/oauth2/resource_server.py +++ b/gn3/auth/authorisation/oauth2/resource_server.py @@ -5,7 +5,7 @@ from authlib.oauth2.rfc6750 import BearerTokenValidator as _BearerTokenValidator from authlib.integrations.flask_oauth2 import ResourceProtector from gn3.auth import db -from gn3.auth.authentication.oauth2.models.oauth2token import token_by_access_token +from gn3.auth.authorisation.oauth2.oauth2token import token_by_access_token class BearerTokenValidator(_BearerTokenValidator): """Extends `authlib.oauth2.rfc6750.BearerTokenValidator`""" diff --git a/gn3/auth/authorisation/privileges.py b/gn3/auth/authorisation/privileges.py index dbb4129..7907d76 100644 --- a/gn3/auth/authorisation/privileges.py +++ b/gn3/auth/authorisation/privileges.py @@ -2,7 +2,7 @@ from typing import Any, Iterable, NamedTuple from gn3.auth import db -from gn3.auth.authentication.users import User +from gn3.auth.authorisation.users import User class Privilege(NamedTuple): """Class representing a privilege: creates immutable objects.""" diff --git a/gn3/auth/authorisation/resources/checks.py b/gn3/auth/authorisation/resources/checks.py index fafde76..1f5a0f9 100644 --- a/gn3/auth/authorisation/resources/checks.py +++ b/gn3/auth/authorisation/resources/checks.py @@ -4,7 +4,7 @@ from functools import reduce from typing import Sequence from gn3.auth import db -from gn3.auth.authentication.users import User +from gn3.auth.authorisation.users import User def __organise_privileges_by_resource_id__(rows): def __organise__(privs, row): diff --git a/gn3/auth/authorisation/resources/models.py b/gn3/auth/authorisation/resources/models.py index b301a93..cf7769e 100644 --- a/gn3/auth/authorisation/resources/models.py +++ b/gn3/auth/authorisation/resources/models.py @@ -7,7 +7,7 @@ from typing import Any, Dict, Sequence, Optional, NamedTuple from gn3.auth import db from gn3.auth.dictify import dictify -from gn3.auth.authentication.users import User +from gn3.auth.authorisation.users import User from gn3.auth.db_utils import with_db_connection from .checks import authorised_for diff --git a/gn3/auth/authorisation/resources/views.py b/gn3/auth/authorisation/resources/views.py index 3b2bbeb..bda67cd 100644 --- a/gn3/auth/authorisation/resources/views.py +++ b/gn3/auth/authorisation/resources/views.py @@ -7,6 +7,8 @@ from functools import reduce from flask import request, jsonify, Response, Blueprint, current_app as app from gn3.auth.db_utils import with_db_connection +from gn3.auth.authorisation.oauth2.resource_server import require_oauth +from gn3.auth.authorisation.users import User, user_by_id, user_by_email from .checks import authorised_for from .models import ( @@ -21,8 +23,6 @@ from ..groups.models import Group, GroupRole, group_role_by_id from ... import db from ...dictify import dictify -from ...authentication.oauth2.resource_server import require_oauth -from ...authentication.users import User, user_by_id, user_by_email resources = Blueprint("resources", __name__) diff --git a/gn3/auth/authorisation/roles/models.py b/gn3/auth/authorisation/roles/models.py index 97e11af..890d33b 100644 --- a/gn3/auth/authorisation/roles/models.py +++ b/gn3/auth/authorisation/roles/models.py @@ -7,7 +7,7 @@ from pymonad.either import Left, Right, Either from gn3.auth import db from gn3.auth.dictify import dictify -from gn3.auth.authentication.users import User +from gn3.auth.authorisation.users import User from gn3.auth.authorisation.errors import AuthorisationError from ..checks import authorised_p diff --git a/gn3/auth/authorisation/roles/views.py b/gn3/auth/authorisation/roles/views.py index 3670aab..d00e596 100644 --- a/gn3/auth/authorisation/roles/views.py +++ b/gn3/auth/authorisation/roles/views.py @@ -5,11 +5,10 @@ from flask import jsonify, Response, Blueprint, current_app from gn3.auth import db from gn3.auth.dictify import dictify +from gn3.auth.authorisation.oauth2.resource_server import require_oauth from .models import user_role -from ...authentication.oauth2.resource_server import require_oauth - roles = Blueprint("roles", __name__) @roles.route("/view/<uuid:role_id>", methods=["GET"]) diff --git a/gn3/auth/authorisation/users/__init__.py b/gn3/auth/authorisation/users/__init__.py index e69de29..5f0c89c 100644 --- a/gn3/auth/authorisation/users/__init__.py +++ b/gn3/auth/authorisation/users/__init__.py @@ -0,0 +1,12 @@ +"""Initialise the users' package.""" +from .base import ( + User, + users, + save_user, + user_by_id, + # valid_login, + user_by_email, + hash_password, # only used in tests... maybe make gn-auth a GN3 dependency + same_password, + set_user_password +) diff --git a/gn3/auth/authorisation/users/admin/__init__.py b/gn3/auth/authorisation/users/admin/__init__.py deleted file mode 100644 index 8aa0743..0000000 --- a/gn3/auth/authorisation/users/admin/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -"""The admin module""" -from .views import admin diff --git a/gn3/auth/authorisation/users/admin/ui.py b/gn3/auth/authorisation/users/admin/ui.py deleted file mode 100644 index 242c7a6..0000000 --- a/gn3/auth/authorisation/users/admin/ui.py +++ /dev/null @@ -1,27 +0,0 @@ -"""UI utilities for the auth system.""" -from functools import wraps -from flask import flash, url_for, redirect - -from gn3.auth.authentication.users import User -from gn3.auth.db_utils import with_db_connection -from gn3.auth.authorisation.roles.models import user_roles - -from gn3.session import logged_in, session_user, clear_session_info - -def is_admin(func): - """Verify user is a system admin.""" - @wraps(func) - @logged_in - def __admin__(*args, **kwargs): - admin_roles = [ - role for role in with_db_connection( - lambda conn: user_roles( - conn, User(**session_user()))) - if role.role_name == "system-administrator"] - if len(admin_roles) > 0: - return func(*args, **kwargs) - flash("Expected a system administrator.", "alert-danger") - flash("You have been logged out of the system.", "alert-info") - clear_session_info() - return redirect(url_for("oauth2.admin.login")) - return __admin__ diff --git a/gn3/auth/authorisation/users/admin/views.py b/gn3/auth/authorisation/users/admin/views.py deleted file mode 100644 index c9f1887..0000000 --- a/gn3/auth/authorisation/users/admin/views.py +++ /dev/null @@ -1,230 +0,0 @@ -"""UI for admin stuff""" -import uuid -import json -import random -import string -from functools import partial -from datetime import datetime, timezone, timedelta - -from email_validator import validate_email, EmailNotValidError -from flask import ( - flash, - request, - url_for, - redirect, - Blueprint, - current_app, - render_template) - - -from gn3 import session -from gn3.auth import db -from gn3.auth.db_utils import with_db_connection - -from gn3.auth.authentication.oauth2.models.oauth2client import ( - save_client, - OAuth2Client, - oauth2_clients, - client as oauth2_client, - delete_client as _delete_client) -from gn3.auth.authentication.users import ( - User, - user_by_id, - valid_login, - user_by_email, - hash_password) - -from .ui import is_admin - -admin = Blueprint("admin", __name__) - -@admin.before_request -def update_expires(): - """Update session expiration.""" - if session.session_info() and not session.update_expiry(): - flash("Session has expired. Logging out...", "alert-warning") - session.clear_session_info() - return redirect(url_for("oauth2.admin.login")) - return None - -@admin.route("/dashboard", methods=["GET"]) -@is_admin -def dashboard(): - """Admin dashboard.""" - return render_template("admin/dashboard.html") - -@admin.route("/login", methods=["GET", "POST"]) -def login(): - """Log in to GN3 directly without OAuth2 client.""" - if request.method == "GET": - return render_template( - "admin/login.html", - next_uri=request.args.get("next", "oauth2.admin.dashboard")) - - form = request.form - next_uri = form.get("next_uri", "oauth2.admin.dashboard") - error_message = "Invalid email or password provided." - login_page = redirect(url_for("oauth2.admin.login", next=next_uri)) - try: - email = validate_email(form.get("email", "").strip(), - check_deliverability=False) - password = form.get("password") - with db.connection(current_app.config["AUTH_DB"]) as conn: - user = user_by_email(conn, email["email"]) - if valid_login(conn, user, password): - session.update_session_info( - user=user._asdict(), - expires=( - datetime.now(tz=timezone.utc) + timedelta(minutes=10))) - return redirect(url_for(next_uri)) - flash(error_message, "alert-danger") - return login_page - except EmailNotValidError as _enve: - flash(error_message, "alert-danger") - return login_page - -@admin.route("/logout", methods=["GET"]) -def logout(): - """Log out the admin.""" - if not session.session_info(): - flash("Not logged in.", "alert-info") - return redirect(url_for("oauth2.admin.login")) - session.clear_session_info() - flash("Logged out", "alert-success") - return redirect(url_for("oauth2.admin.login")) - -def random_string(length: int = 64) -> str: - """Generate a random string.""" - return "".join( - random.choice(string.ascii_letters + string.digits + string.punctuation) - for _idx in range(0, length)) - -def __response_types__(grant_types: tuple[str, ...]) -> tuple[str, ...]: - """Compute response types from grant types.""" - resps = { - "password": ("token",), - "authorization_code": ("token", "code"), - "refresh_token": ("token",) - } - return tuple(set( - resp_typ for types_list - in (types for grant, types in resps.items() if grant in grant_types) - for resp_typ in types_list)) - -@admin.route("/register-client", methods=["GET", "POST"]) -@is_admin -def register_client(): - """Register an OAuth2 client.""" - def __list_users__(conn): - with db.cursor(conn) as cursor: - cursor.execute("SELECT * FROM users") - return tuple( - User(uuid.UUID(row["user_id"]), row["email"], row["name"]) - for row in cursor.fetchall()) - if request.method == "GET": - return render_template( - "admin/register-client.html", - scope=current_app.config["OAUTH2_SCOPE"], - users=with_db_connection(__list_users__), - current_user=session.session_user()) - - form = request.form - raw_client_secret = random_string() - default_redirect_uri = form["redirect_uri"] - grant_types = form.getlist("grants[]") - client = OAuth2Client( - client_id = uuid.uuid4(), - client_secret = hash_password(raw_client_secret), - client_id_issued_at = datetime.now(tz=timezone.utc), - client_secret_expires_at = datetime.fromtimestamp(0), - client_metadata = { - "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": default_redirect_uri, - "redirect_uris": [default_redirect_uri] + form.get("other_redirect_uri", "").split(), - "response_type": __response_types__(tuple(grant_types)), - "scope": form.getlist("scope[]") - }, - user = with_db_connection(partial( - user_by_id, user_id=uuid.UUID(form["user"]))) - ) - client = with_db_connection(partial(save_client, the_client=client)) - return render_template( - "admin/registered-client.html", - client=client, - client_secret = raw_client_secret) - -def __parse_client__(sqlite3_row) -> dict: - """Parse the client details into python datatypes.""" - return { - **dict(sqlite3_row), - "client_metadata": json.loads(sqlite3_row["client_metadata"]) - } - -@admin.route("/list-client", methods=["GET"]) -@is_admin -def list_clients(): - """List all registered OAuth2 clients.""" - return render_template( - "admin/list-oauth2-clients.html", - clients=with_db_connection(oauth2_clients)) - -@admin.route("/view-client/<uuid:client_id>", methods=["GET"]) -@is_admin -def view_client(client_id: uuid.UUID): - """View details of OAuth2 client with given `client_id`.""" - return render_template( - "admin/view-oauth2-client.html", - client=with_db_connection(partial(oauth2_client, client_id=client_id)), - scope=current_app.config["OAUTH2_SCOPE"]) - -@admin.route("/edit-client", methods=["POST"]) -@is_admin -def edit_client(): - """Edit the details of the given client.""" - form = request.form - the_client = with_db_connection(partial( - oauth2_client, client_id=uuid.UUID(form["client_id"]))) - if the_client.is_nothing(): - flash("No such client.", "alert-danger") - return redirect(url_for("oauth2.admin.list_clients")) - the_client = the_client.value - client_metadata = { - **the_client.client_metadata, - "default_redirect_uri": form["default_redirect_uri"], - "redirect_uris": list(set( - [form["default_redirect_uri"]] + - form["other_redirect_uris"].split("\r\n"))), - "grants": form.getlist("grants[]"), - "scope": form.getlist("scope[]") - } - with_db_connection(partial(save_client, the_client=OAuth2Client( - the_client.client_id, - the_client.client_secret, - the_client.client_id_issued_at, - the_client.client_secret_expires_at, - client_metadata, - the_client.user))) - flash("Client updated.", "alert-success") - return redirect(url_for("oauth2.admin.view_client", - client_id=the_client.client_id)) - -@admin.route("/delete-client", methods=["POST"]) -@is_admin -def delete_client(): - """Delete the details of the client.""" - form = request.form - the_client = with_db_connection(partial( - oauth2_client, client_id=uuid.UUID(form["client_id"]))) - if the_client.is_nothing(): - flash("No such client.", "alert-danger") - return redirect(url_for("oauth2.admin.list_clients")) - the_client = the_client.value - with_db_connection(partial(_delete_client, client=the_client)) - flash((f"Client '{the_client.client_metadata.client_name}' was deleted " - "successfully."), - "alert-success") - return redirect(url_for("oauth2.admin.list_clients")) diff --git a/gn3/auth/authentication/users.py b/gn3/auth/authorisation/users/base.py index 0e72ed2..0e72ed2 100644 --- a/gn3/auth/authentication/users.py +++ b/gn3/auth/authorisation/users/base.py diff --git a/gn3/auth/authorisation/users/collections/views.py b/gn3/auth/authorisation/users/collections/views.py index 1fa25a3..775e8bc 100644 --- a/gn3/auth/authorisation/users/collections/views.py +++ b/gn3/auth/authorisation/users/collections/views.py @@ -9,8 +9,8 @@ from gn3.auth.db_utils import with_db_connection from gn3.auth.authorisation.checks import require_json from gn3.auth.authorisation.errors import NotFoundError -from gn3.auth.authentication.users import User, user_by_id -from gn3.auth.authentication.oauth2.resource_server import require_oauth +from gn3.auth.authorisation.users import User, user_by_id +from gn3.auth.authorisation.oauth2.resource_server import require_oauth from .models import ( add_traits, diff --git a/gn3/auth/authorisation/users/masquerade/__init__.py b/gn3/auth/authorisation/users/masquerade/__init__.py deleted file mode 100644 index 69d64f0..0000000 --- a/gn3/auth/authorisation/users/masquerade/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Package to deal with masquerading.""" diff --git a/gn3/auth/authorisation/users/masquerade/models.py b/gn3/auth/authorisation/users/masquerade/models.py deleted file mode 100644 index 9f24b6b..0000000 --- a/gn3/auth/authorisation/users/masquerade/models.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Functions for handling masquerade.""" -from uuid import uuid4 -from functools import wraps -from datetime import datetime - -from flask import current_app as app - -from gn3.auth import db - -from gn3.auth.authorisation.errors import ForbiddenAccess -from gn3.auth.authorisation.roles.models import user_roles - -from gn3.auth.authentication.users import User -from gn3.auth.authentication.oauth2.models.oauth2token import ( - OAuth2Token, save_token) - -__FIVE_HOURS__ = (60 * 60 * 5) - -def can_masquerade(func): - """Security decorator.""" - @wraps(func) - def __checker__(*args, **kwargs): - if len(args) == 3: - conn, token, _masq_user = args - elif len(args) == 2: - conn, token = args - elif len(args) == 1: - conn = args[0] - token = kwargs["original_token"] - else: - conn = kwargs["conn"] - token = kwargs["original_token"] - - masq_privs = [priv for role in user_roles(conn, token.user) - for priv in role.privileges - if priv.privilege_id == "system:user:masquerade"] - if len(masq_privs) == 0: - raise ForbiddenAccess( - "You do not have the ability to masquerade as another user.") - return func(*args, **kwargs) - return __checker__ - -@can_masquerade -def masquerade_as( - conn: db.DbConnection, - original_token: OAuth2Token, - masqueradee: User) -> OAuth2Token: - """Get a token that enables `masquerader` to act as `masqueradee`.""" - token_details = app.config["OAUTH2_SERVER"].generate_token( - client=original_token.client, - grant_type="authorization_code", - user=masqueradee, - expires_in=__FIVE_HOURS__, - include_refresh_token=True) - new_token = OAuth2Token( - token_id=uuid4(), - client=original_token.client, - token_type=token_details["token_type"], - access_token=token_details["access_token"], - refresh_token=token_details.get("refresh_token"), - scope=original_token.scope, - revoked=False, - issued_at=datetime.now(), - expires_in=token_details["expires_in"], - user=masqueradee) - save_token(conn, new_token) - return new_token diff --git a/gn3/auth/authorisation/users/masquerade/views.py b/gn3/auth/authorisation/users/masquerade/views.py deleted file mode 100644 index 43286a1..0000000 --- a/gn3/auth/authorisation/users/masquerade/views.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Endpoints for user masquerade""" -from uuid import UUID -from functools import partial - -from flask import request, jsonify, Response, Blueprint - -from gn3.auth.db_utils import with_db_connection -from gn3.auth.authorisation.errors import InvalidData -from gn3.auth.authorisation.checks import require_json - -from gn3.auth.authentication.users import user_by_id -from gn3.auth.authentication.oauth2.resource_server import require_oauth - -from .models import masquerade_as - -masq = Blueprint("masquerade", __name__) - -@masq.route("/", methods=["POST"]) -@require_oauth("profile user masquerade") -@require_json -def masquerade() -> Response: - """Masquerade as a particular user.""" - with require_oauth.acquire("profile user masquerade") as token: - masqueradee_id = UUID(request.json["masquerade_as"])#type: ignore[index] - if masqueradee_id == token.user.user_id: - raise InvalidData("You are not allowed to masquerade as yourself.") - - masq_user = with_db_connection(partial( - user_by_id, user_id=masqueradee_id)) - def __masq__(conn): - new_token = masquerade_as(conn, original_token=token, masqueradee=masq_user) - return new_token - def __dump_token__(tok): - return { - key: value for key, value in (tok._asdict().items()) - if key in ("access_token", "refresh_token", "expires_in", - "token_type") - } - return jsonify({ - "original": { - "user": token.user._asdict(), - "token": __dump_token__(token) - }, - "masquerade_as": { - "user": masq_user._asdict(), - "token": __dump_token__(with_db_connection(__masq__)) - } - }) diff --git a/gn3/auth/authorisation/users/models.py b/gn3/auth/authorisation/users/models.py index 89c1d22..0157154 100644 --- a/gn3/auth/authorisation/users/models.py +++ b/gn3/auth/authorisation/users/models.py @@ -7,7 +7,7 @@ from gn3.auth.authorisation.roles.models import Role from gn3.auth.authorisation.checks import authorised_p from gn3.auth.authorisation.privileges import Privilege -from gn3.auth.authentication.users import User +from .base import User @authorised_p( ("system:user:list",), diff --git a/gn3/auth/authorisation/users/views.py b/gn3/auth/authorisation/users/views.py index 826e222..f75b51e 100644 --- a/gn3/auth/authorisation/users/views.py +++ b/gn3/auth/authorisation/users/views.py @@ -10,9 +10,11 @@ from flask import request, jsonify, Response, Blueprint, current_app from gn3.auth import db from gn3.auth.dictify import dictify from gn3.auth.db_utils import with_db_connection +from gn3.auth.authorisation.oauth2.resource_server import require_oauth +from gn3.auth.authorisation.users import User, save_user, set_user_password +from gn3.auth.authorisation.oauth2.oauth2token import token_by_access_token from .models import list_users -from .masquerade.views import masq from .collections.views import collections from ..groups.models import user_group as _user_group @@ -21,12 +23,7 @@ from ..roles.models import assign_default_roles, user_roles as _user_roles from ..errors import ( NotFoundError, UsernameError, PasswordError, UserRegistrationError) -from ...authentication.oauth2.resource_server import require_oauth -from ...authentication.users import User, save_user, set_user_password -from ...authentication.oauth2.models.oauth2token import token_by_access_token - users = Blueprint("users", __name__) -users.register_blueprint(masq, url_prefix="/masquerade") users.register_blueprint(collections, url_prefix="/collections") @users.route("/", methods=["GET"]) diff --git a/gn3/auth/views.py b/gn3/auth/views.py index 56eace7..da64049 100644 --- a/gn3/auth/views.py +++ b/gn3/auth/views.py @@ -1,21 +1,16 @@ """The Auth(oris|entic)ation routes/views""" from flask import Blueprint -from .authentication.oauth2.views import auth - from .authorisation.data.views import data from .authorisation.users.views import users -from .authorisation.users.admin import admin from .authorisation.roles.views import roles from .authorisation.groups.views import groups from .authorisation.resources.views import resources oauth2 = Blueprint("oauth2", __name__) -oauth2.register_blueprint(auth, url_prefix="/") oauth2.register_blueprint(data, url_prefix="/data") oauth2.register_blueprint(users, url_prefix="/user") oauth2.register_blueprint(roles, url_prefix="/role") -oauth2.register_blueprint(admin, url_prefix="/admin") oauth2.register_blueprint(groups, url_prefix="/group") oauth2.register_blueprint(resources, url_prefix="/resource") @@ -3,7 +3,6 @@ import sys import uuid import json from math import ceil -from pathlib import Path from datetime import datetime @@ -12,13 +11,10 @@ from yoyo import get_backend, read_migrations from gn3 import migrations from gn3.app import create_app -from gn3.auth.authentication.users import hash_password +from gn3.auth.authorisation.users import hash_password from gn3.auth import db -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 ##### @@ -112,16 +108,6 @@ def assign_system_admin(user_id: uuid.UUID): 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__': diff --git a/migrations/auth/20221103_01_js9ub-initialise-the-auth-entic-oris-ation-database.py b/migrations/auth/20221103_01_js9ub-initialise-the-auth-entic-oris-ation-database.py deleted file mode 100644 index d511f5d..0000000 --- a/migrations/auth/20221103_01_js9ub-initialise-the-auth-entic-oris-ation-database.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -Initialise the auth(entic|oris)ation database. -""" - -from yoyo import step - -__depends__ = {} # type: ignore[var-annotated] - -steps = [ - step( - """ - CREATE TABLE IF NOT EXISTS users( - user_id TEXT PRIMARY KEY NOT NULL, - email TEXT UNIQUE NOT NULL, - name TEXT - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS users") -] diff --git a/migrations/auth/20221103_02_sGrIs-create-user-credentials-table.py b/migrations/auth/20221103_02_sGrIs-create-user-credentials-table.py deleted file mode 100644 index 48bd663..0000000 --- a/migrations/auth/20221103_02_sGrIs-create-user-credentials-table.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -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) - ON UPDATE CASCADE ON DELETE RESTRICT - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS user_credentials") -] diff --git a/migrations/auth/20221108_01_CoxYh-create-the-groups-table.py b/migrations/auth/20221108_01_CoxYh-create-the-groups-table.py deleted file mode 100644 index 29f92d4..0000000 --- a/migrations/auth/20221108_01_CoxYh-create-the-groups-table.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -Create the groups table -""" - -from yoyo import step - -__depends__ = {'20221103_02_sGrIs-create-user-credentials-table'} - -steps = [ - step( - """ - CREATE TABLE IF NOT EXISTS groups( - group_id TEXT PRIMARY KEY NOT NULL, - group_name TEXT NOT NULL, - group_metadata TEXT - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS groups") -] diff --git a/migrations/auth/20221108_02_wxTr9-create-privileges-table.py b/migrations/auth/20221108_02_wxTr9-create-privileges-table.py deleted file mode 100644 index 67720b2..0000000 --- a/migrations/auth/20221108_02_wxTr9-create-privileges-table.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -Create privileges table -""" - -from yoyo import step - -__depends__ = {'20221108_01_CoxYh-create-the-groups-table'} - -steps = [ - step( - """ - CREATE TABLE privileges( - privilege_id TEXT PRIMARY KEY, - privilege_name TEXT NOT NULL - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS privileges") -] diff --git a/migrations/auth/20221108_03_Pbhb1-create-resource-categories-table.py b/migrations/auth/20221108_03_Pbhb1-create-resource-categories-table.py deleted file mode 100644 index ce752ef..0000000 --- a/migrations/auth/20221108_03_Pbhb1-create-resource-categories-table.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -Create resource_categories table -""" - -from yoyo import step - -__depends__ = {'20221108_02_wxTr9-create-privileges-table'} - -steps = [ - step( - """ - CREATE TABLE resource_categories( - resource_category_id TEXT PRIMARY KEY, - resource_category_key TEXT NOT NULL, - resource_category_description TEXT NOT NULL - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS resource_categories") -] diff --git a/migrations/auth/20221108_04_CKcSL-init-data-in-resource-categories-table.py b/migrations/auth/20221108_04_CKcSL-init-data-in-resource-categories-table.py deleted file mode 100644 index 76ffbef..0000000 --- a/migrations/auth/20221108_04_CKcSL-init-data-in-resource-categories-table.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -Init data in resource_categories table -""" - -from yoyo import step - -__depends__ = {'20221108_03_Pbhb1-create-resource-categories-table'} - -steps = [ - step( - """ - INSERT INTO resource_categories VALUES - ('fad071a3-2fc8-40b8-992b-cdefe7dcac79', 'mrna', 'mRNA Dataset'), - ('548d684b-d4d1-46fb-a6d3-51a56b7da1b3', 'phenotype', 'Phenotype (Publish) Dataset'), - ('48056f84-a2a6-41ac-8319-0e1e212cba2a', 'genotype', 'Genotype Dataset') - """, - """ - DELETE FROM resource_categories WHERE resource_category_id IN - ( - 'fad071a3-2fc8-40b8-992b-cdefe7dcac79', - '548d684b-d4d1-46fb-a6d3-51a56b7da1b3', - '48056f84-a2a6-41ac-8319-0e1e212cba2a' - ) - """) -] diff --git a/migrations/auth/20221109_01_HbD5F-add-resource-meta-field-to-resource-categories-field.py b/migrations/auth/20221109_01_HbD5F-add-resource-meta-field-to-resource-categories-field.py deleted file mode 100644 index 6c829b1..0000000 --- a/migrations/auth/20221109_01_HbD5F-add-resource-meta-field-to-resource-categories-field.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -Add 'resource_meta' field to 'resource_categories' field. -""" - -from yoyo import step - -__depends__ = {'20221108_04_CKcSL-init-data-in-resource-categories-table'} - -steps = [ - step( - """ - ALTER TABLE resource_categories - ADD COLUMN - resource_meta TEXT NOT NULL DEFAULT '[]' - """, - "ALTER TABLE resource_categories DROP COLUMN resource_meta") -] diff --git a/migrations/auth/20221110_01_WtZ1I-create-resources-table.py b/migrations/auth/20221110_01_WtZ1I-create-resources-table.py deleted file mode 100644 index abc8895..0000000 --- a/migrations/auth/20221110_01_WtZ1I-create-resources-table.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Create 'resources' table -""" - -from yoyo import step - -__depends__ = {'20221109_01_HbD5F-add-resource-meta-field-to-resource-categories-field'} - -steps = [ - step( - """ - CREATE TABLE IF NOT EXISTS resources( - group_id TEXT NOT NULL, - resource_id TEXT NOT NULL, - resource_name TEXT NOT NULL UNIQUE, - resource_category_id TEXT NOT NULL, - PRIMARY KEY(group_id, resource_id), - FOREIGN KEY(group_id) REFERENCES groups(group_id) - ON UPDATE CASCADE ON DELETE RESTRICT, - FOREIGN KEY(resource_category_id) - REFERENCES resource_categories(resource_category_id) - ON UPDATE CASCADE ON DELETE RESTRICT - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS resources") -] diff --git a/migrations/auth/20221110_05_BaNtL-create-roles-table.py b/migrations/auth/20221110_05_BaNtL-create-roles-table.py deleted file mode 100644 index 51e19e8..0000000 --- a/migrations/auth/20221110_05_BaNtL-create-roles-table.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -Create 'roles' table -""" - -from yoyo import step - -__depends__ = {'20221110_01_WtZ1I-create-resources-table'} - -steps = [ - step( - """ - CREATE TABLE IF NOT EXISTS roles( - role_id TEXT NOT NULL PRIMARY KEY, - role_name TEXT NOT NULL, - user_editable INTEGER NOT NULL DEFAULT 1 CHECK (user_editable=0 or user_editable=1) - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS roles") -] diff --git a/migrations/auth/20221110_06_Pq2kT-create-generic-roles-table.py b/migrations/auth/20221110_06_Pq2kT-create-generic-roles-table.py deleted file mode 100644 index 2b55c2b..0000000 --- a/migrations/auth/20221110_06_Pq2kT-create-generic-roles-table.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -Create 'generic_roles' table - -The roles in this table will be template roles, defining some common roles that -can be used within the groups. - -They could also be used to define system-level roles, though those will not be -provided to the "common" users. -""" - -from yoyo import step - -__depends__ = {'20221110_05_BaNtL-create-roles-table'} - -steps = [ - step( - """ - CREATE TABLE IF NOT EXISTS generic_roles( - role_id TEXT PRIMARY KEY, - role_name TEXT NOT NULL - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS generic_roles") -] diff --git a/migrations/auth/20221110_07_7WGa1-create-role-privileges-table.py b/migrations/auth/20221110_07_7WGa1-create-role-privileges-table.py deleted file mode 100644 index 0d0eeb9..0000000 --- a/migrations/auth/20221110_07_7WGa1-create-role-privileges-table.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Create 'role_privileges' table -""" - -from yoyo import step - -__depends__ = {'20221110_06_Pq2kT-create-generic-roles-table'} - -steps = [ - step( - """ - CREATE TABLE IF NOT EXISTS role_privileges( - role_id TEXT NOT NULL, - privilege_id TEXT NOT NULL, - PRIMARY KEY(role_id, privilege_id), - FOREIGN KEY(role_id) REFERENCES roles(role_id) - ON UPDATE CASCADE ON DELETE RESTRICT, - FOREIGN KEY(privilege_id) REFERENCES privileges(privilege_id) - ON UPDATE CASCADE ON DELETE RESTRICT - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS role_privileges"), - step( - """ - CREATE INDEX IF NOT EXISTS idx_tbl_role_privileges_cols_role_id - ON role_privileges(role_id) - """, - "DROP INDEX IF EXISTS idx_tbl_role_privileges_cols_role_id") -] diff --git a/migrations/auth/20221110_08_23psB-add-privilege-category-and-privilege-description-columns-to-privileges-table.py b/migrations/auth/20221110_08_23psB-add-privilege-category-and-privilege-description-columns-to-privileges-table.py deleted file mode 100644 index 077182b..0000000 --- a/migrations/auth/20221110_08_23psB-add-privilege-category-and-privilege-description-columns-to-privileges-table.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -Add 'privilege_category' and 'privilege_description' columns to 'privileges' table -""" - -from yoyo import step - -__depends__ = {'20221110_07_7WGa1-create-role-privileges-table'} - -steps = [ - step( - """ - ALTER TABLE privileges ADD COLUMN - privilege_category TEXT NOT NULL DEFAULT 'common' - """, - "ALTER TABLE privileges DROP COLUMN privilege_category"), - step( - """ - ALTER TABLE privileges ADD COLUMN - privilege_description TEXT - """, - "ALTER TABLE privileges DROP COLUMN privilege_description") -] diff --git a/migrations/auth/20221113_01_7M0hv-enumerate-initial-privileges.py b/migrations/auth/20221113_01_7M0hv-enumerate-initial-privileges.py deleted file mode 100644 index 072f226..0000000 --- a/migrations/auth/20221113_01_7M0hv-enumerate-initial-privileges.py +++ /dev/null @@ -1,66 +0,0 @@ -""" -Enumerate initial privileges -""" - -from yoyo import step - -__depends__ = {'20221110_08_23psB-add-privilege-category-and-privilege-description-columns-to-privileges-table'} - -steps = [ - step( - """ - INSERT INTO - privileges(privilege_id, privilege_name, privilege_category, - privilege_description) - VALUES - -- group-management privileges - ('4842e2aa-38b9-4349-805e-0a99a9cf8bff', 'create-group', - 'group-management', 'Create a group'), - ('3ebfe79c-d159-4629-8b38-772cf4bc2261', 'view-group', - 'group-management', 'View the details of a group'), - ('52576370-b3c7-4e6a-9f7e-90e9dbe24d8f', 'edit-group', - 'group-management', 'Edit the details of a group'), - ('13ec2a94-4f1a-442d-aad2-936ad6dd5c57', 'delete-group', - 'group-management', 'Delete a group'), - ('ae4add8c-789a-4d11-a6e9-a306470d83d9', 'add-group-member', - 'group-management', 'Add a user to a group'), - ('f1bd3f42-567e-4965-9643-6d1a52ddee64', 'remove-group-member', - 'group-management', 'Remove a user from a group'), - ('80f11285-5079-4ec0-907c-06509f88a364', 'assign-group-leader', - 'group-management', 'Assign user group-leader privileges'), - ('d4afe2b3-4ca0-4edd-b37d-966535b5e5bd', - 'transfer-group-leadership', 'group-management', - 'Transfer leadership of the group to some other member'), - - -- resource-management privileges - ('aa25b32a-bff2-418d-b0a2-e26b4a8f089b', 'create-resource', - 'resource-management', 'Create a resource object'), - ('7f261757-3211-4f28-a43f-a09b800b164d', 'view-resource', - 'resource-management', 'view a resource and use it in computations'), - ('2f980855-959b-4339-b80e-25d1ec286e21', 'edit-resource', - 'resource-management', 'edit/update a resource'), - ('d2a070fd-e031-42fb-ba41-d60cf19e5d6d', 'delete-resource', - 'resource-management', 'Delete a resource'), - - -- role-management privileges - ('221660b1-df05-4be1-b639-f010269dbda9', 'create-role', - 'role-management', 'Create a new role'), - ('7bcca363-cba9-4169-9e31-26bdc6179b28', 'edit-role', - 'role-management', 'edit/update an existing role'), - ('5103cc68-96f8-4ebb-83a4-a31692402c9b', 'assign-role', - 'role-management', 'Assign a role to an existing user'), - ('1c59eff5-9336-4ed2-a166-8f70d4cb012e', 'delete-role', - 'role-management', 'Delete an existing role'), - - -- user-management privileges - ('e7252301-6ee0-43ba-93ef-73b607cf06f6', 'reset-any-password', - 'user-management', 'Reset the password for any user'), - ('1fe61370-cae9-4983-bd6c-ce61050c510f', 'delete-any-user', - 'user-management', 'Delete any user from the system'), - - -- sytem-admin privileges - ('519db546-d44e-4fdc-9e4e-25aa67548ab3', 'masquerade', - 'system-admin', 'Masquerade as some other user') - """, - "DELETE FROM privileges") -] diff --git a/migrations/auth/20221114_01_n8gsF-create-generic-role-privileges-table.py b/migrations/auth/20221114_01_n8gsF-create-generic-role-privileges-table.py deleted file mode 100644 index 2048f4a..0000000 --- a/migrations/auth/20221114_01_n8gsF-create-generic-role-privileges-table.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Create 'generic_role_privileges' table - -This table links the generic_roles to the privileges they provide -""" - -from yoyo import step - -__depends__ = {'20221113_01_7M0hv-enumerate-initial-privileges'} - -steps = [ - step( - """ - CREATE TABLE IF NOT EXISTS generic_role_privileges( - generic_role_id TEXT NOT NULL, - privilege_id TEXT NOT NULL, - PRIMARY KEY(generic_role_id, privilege_id), - FOREIGN KEY(generic_role_id) REFERENCES generic_roles(role_id) - ON UPDATE CASCADE ON DELETE RESTRICT, - FOREIGN KEY(privilege_id) REFERENCES privileges(privilege_id) - ON UPDATE CASCADE ON DELETE RESTRICT - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS generic_role_privileges"), - step( - """ - CREATE INDEX IF NOT EXISTS - idx_tbl_generic_role_privileges_cols_generic_role_id - ON generic_role_privileges(generic_role_id) - """, - """ - DROP INDEX IF EXISTS - idx_tbl_generic_role_privileges_cols_generic_role_id - """) -] diff --git a/migrations/auth/20221114_02_DKKjn-drop-generic-role-tables.py b/migrations/auth/20221114_02_DKKjn-drop-generic-role-tables.py deleted file mode 100644 index 6bd101b..0000000 --- a/migrations/auth/20221114_02_DKKjn-drop-generic-role-tables.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -Drop 'generic_role*' tables -""" - -from yoyo import step - -__depends__ = {'20221114_01_n8gsF-create-generic-role-privileges-table'} - -steps = [ - step( - """ - DROP INDEX IF EXISTS - idx_tbl_generic_role_privileges_cols_generic_role_id - """, - """ - CREATE INDEX IF NOT EXISTS - idx_tbl_generic_role_privileges_cols_generic_role_id - ON generic_role_privileges(generic_role_id) - """), - step( - "DROP TABLE IF EXISTS generic_role_privileges", - """ - CREATE TABLE IF NOT EXISTS generic_role_privileges( - generic_role_id TEXT NOT NULL, - privilege_id TEXT NOT NULL, - PRIMARY KEY(generic_role_id, privilege_id), - FOREIGN KEY(generic_role_id) REFERENCES generic_roles(role_id) - ON UPDATE CASCADE ON DELETE RESTRICT, - FOREIGN KEY(privilege_id) REFERENCES privileges(privilege_id) - ON UPDATE CASCADE ON DELETE RESTRICT - ) WITHOUT ROWID - """), - step( - "DROP TABLE IF EXISTS generic_roles", - """ - CREATE TABLE IF NOT EXISTS generic_roles( - role_id TEXT PRIMARY KEY, - role_name TEXT NOT NULL - ) WITHOUT ROWID - """) -] diff --git a/migrations/auth/20221114_03_PtWjc-create-group-roles-table.py b/migrations/auth/20221114_03_PtWjc-create-group-roles-table.py deleted file mode 100644 index a7e7b45..0000000 --- a/migrations/auth/20221114_03_PtWjc-create-group-roles-table.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Create 'group_roles' table -""" - -from yoyo import step - -__depends__ = {'20221114_02_DKKjn-drop-generic-role-tables'} - -steps = [ - step( - """ - CREATE TABLE IF NOT EXISTS group_roles( - group_id TEXT NOT NULL, - role_id TEXT NOT NULL, - PRIMARY KEY(group_id, role_id), - FOREIGN KEY(group_id) REFERENCES groups(group_id) - ON UPDATE CASCADE ON DELETE RESTRICT, - FOREIGN KEY(role_id) REFERENCES roles(role_id) - ON UPDATE CASCADE ON DELETE RESTRICT - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS group_roles"), - step( - """ - CREATE INDEX IF NOT EXISTS idx_tbl_group_roles_cols_group_id - ON group_roles(group_id) - """, - "DROP INDEX IF EXISTS idx_tbl_group_roles_cols_group_id") -] diff --git a/migrations/auth/20221114_04_tLUzB-initialise-basic-roles.py b/migrations/auth/20221114_04_tLUzB-initialise-basic-roles.py deleted file mode 100644 index 386f481..0000000 --- a/migrations/auth/20221114_04_tLUzB-initialise-basic-roles.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -Initialise basic roles -""" - -from yoyo import step - -__depends__ = {'20221114_03_PtWjc-create-group-roles-table'} - -steps = [ - step( - """ - INSERT INTO roles(role_id, role_name, user_editable) VALUES - ('a0e67630-d502-4b9f-b23f-6805d0f30e30', 'group-leader', '0'), - ('522e4d40-aefc-4a64-b7e0-768b8be517ee', 'resource-owner', '0') - """, - "DELETE FROM roles"), - step( - """ - INSERT INTO role_privileges(role_id, privilege_id) - VALUES - -- group-management - ('a0e67630-d502-4b9f-b23f-6805d0f30e30', - '4842e2aa-38b9-4349-805e-0a99a9cf8bff'), - ('a0e67630-d502-4b9f-b23f-6805d0f30e30', - '3ebfe79c-d159-4629-8b38-772cf4bc2261'), - ('a0e67630-d502-4b9f-b23f-6805d0f30e30', - '52576370-b3c7-4e6a-9f7e-90e9dbe24d8f'), - ('a0e67630-d502-4b9f-b23f-6805d0f30e30', - '13ec2a94-4f1a-442d-aad2-936ad6dd5c57'), - ('a0e67630-d502-4b9f-b23f-6805d0f30e30', - 'ae4add8c-789a-4d11-a6e9-a306470d83d9'), - ('a0e67630-d502-4b9f-b23f-6805d0f30e30', - 'f1bd3f42-567e-4965-9643-6d1a52ddee64'), - ('a0e67630-d502-4b9f-b23f-6805d0f30e30', - 'd4afe2b3-4ca0-4edd-b37d-966535b5e5bd'), - - -- resource-management - ('a0e67630-d502-4b9f-b23f-6805d0f30e30', - 'aa25b32a-bff2-418d-b0a2-e26b4a8f089b'), - ('a0e67630-d502-4b9f-b23f-6805d0f30e30', - '7f261757-3211-4f28-a43f-a09b800b164d'), - ('a0e67630-d502-4b9f-b23f-6805d0f30e30', - '2f980855-959b-4339-b80e-25d1ec286e21'), - ('a0e67630-d502-4b9f-b23f-6805d0f30e30', - 'd2a070fd-e031-42fb-ba41-d60cf19e5d6d'), - ('522e4d40-aefc-4a64-b7e0-768b8be517ee', - 'aa25b32a-bff2-418d-b0a2-e26b4a8f089b'), - ('522e4d40-aefc-4a64-b7e0-768b8be517ee', - '7f261757-3211-4f28-a43f-a09b800b164d'), - ('522e4d40-aefc-4a64-b7e0-768b8be517ee', - '2f980855-959b-4339-b80e-25d1ec286e21'), - ('522e4d40-aefc-4a64-b7e0-768b8be517ee', - 'd2a070fd-e031-42fb-ba41-d60cf19e5d6d') - """, - "DELETE FROM role_privileges") -] diff --git a/migrations/auth/20221114_05_hQun6-create-user-roles-table.py b/migrations/auth/20221114_05_hQun6-create-user-roles-table.py deleted file mode 100644 index e0de751..0000000 --- a/migrations/auth/20221114_05_hQun6-create-user-roles-table.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Create 'user_roles' table. -""" - -from yoyo import step - -__depends__ = {'20221114_04_tLUzB-initialise-basic-roles'} - -steps = [ - step( - """ - CREATE TABLE IF NOT EXISTS user_roles( - user_id TEXT NOT NULL, - role_id TEXT NOT NULL, - PRIMARY KEY(user_id, role_id), - FOREIGN KEY(user_id) REFERENCES users(user_id) - ON UPDATE CASCADE ON DELETE RESTRICT, - FOREIGN KEY(role_id) REFERENCES roles(role_id) - ON UPDATE CASCADE ON DELETE RESTRICT - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS user_roles"), - step( - """ - CREATE INDEX IF NOT EXISTS idx_tbl_user_roles_cols_user_id - ON user_roles(user_id) - """, - "DROP INDEX IF EXISTS idx_tbl_user_roles_cols_user_id") -] diff --git a/migrations/auth/20221116_01_nKUmX-add-privileges-to-group-leader-role.py b/migrations/auth/20221116_01_nKUmX-add-privileges-to-group-leader-role.py deleted file mode 100644 index 2e4ae28..0000000 --- a/migrations/auth/20221116_01_nKUmX-add-privileges-to-group-leader-role.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Add privileges to 'group-leader' role. -""" - -from yoyo import step - -__depends__ = {'20221114_05_hQun6-create-user-roles-table'} - -steps = [ - step( - """ - INSERT INTO role_privileges(role_id, privilege_id) - VALUES - -- role management - ('a0e67630-d502-4b9f-b23f-6805d0f30e30', - '221660b1-df05-4be1-b639-f010269dbda9'), - ('a0e67630-d502-4b9f-b23f-6805d0f30e30', - '7bcca363-cba9-4169-9e31-26bdc6179b28'), - ('a0e67630-d502-4b9f-b23f-6805d0f30e30', - '5103cc68-96f8-4ebb-83a4-a31692402c9b'), - ('a0e67630-d502-4b9f-b23f-6805d0f30e30', - '1c59eff5-9336-4ed2-a166-8f70d4cb012e') - """, - """ - DELETE FROM role_privileges - WHERE - role_id='a0e67630-d502-4b9f-b23f-6805d0f30e30' - AND privilege_id IN ( - '221660b1-df05-4be1-b639-f010269dbda9', - '7bcca363-cba9-4169-9e31-26bdc6179b28', - '5103cc68-96f8-4ebb-83a4-a31692402c9b', - '1c59eff5-9336-4ed2-a166-8f70d4cb012e' - ) - """) -] diff --git a/migrations/auth/20221117_01_RDlfx-modify-group-roles-add-group-role-id.py b/migrations/auth/20221117_01_RDlfx-modify-group-roles-add-group-role-id.py deleted file mode 100644 index a4d7806..0000000 --- a/migrations/auth/20221117_01_RDlfx-modify-group-roles-add-group-role-id.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -Modify 'group_roles': add 'group_role_id' - -At this point, there is no data in the `group_roles` table and therefore, it -should be safe to simply recreate it. -""" - -from yoyo import step - -__depends__ = {'20221116_01_nKUmX-add-privileges-to-group-leader-role'} - -steps = [ - step( - "DROP INDEX IF EXISTS idx_tbl_group_roles_cols_group_id", - """ - CREATE INDEX IF NOT EXISTS idx_tbl_group_roles_cols_group_id - ON group_roles(group_id) - """), - step( - "DROP TABLE IF EXISTS group_roles", - """ - CREATE TABLE IF NOT EXISTS group_roles( - group_id TEXT NOT NULL, - role_id TEXT NOT NULL, - PRIMARY KEY(group_id, role_id), - FOREIGN KEY(group_id) REFERENCES groups(group_id) - ON UPDATE CASCADE ON DELETE RESTRICT, - FOREIGN KEY(role_id) REFERENCES roles(role_id) - ON UPDATE CASCADE ON DELETE RESTRICT - ) WITHOUT ROWID - """), - step( - """ - CREATE TABLE IF NOT EXISTS group_roles( - group_role_id TEXT PRIMARY KEY, - group_id TEXT NOT NULL, - role_id TEXT NOT NULL, - UNIQUE (group_id, role_id), - FOREIGN KEY(group_id) REFERENCES groups(group_id) - ON UPDATE CASCADE ON DELETE RESTRICT, - FOREIGN KEY(role_id) REFERENCES roles(role_id) - ON UPDATE CASCADE ON DELETE RESTRICT - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS group_roles"), - step( - """ - CREATE INDEX IF NOT EXISTS idx_tbl_group_roles_cols_group_id - ON group_roles(group_id) - """, - "DROP INDEX IF EXISTS idx_tbl_group_roles_cols_group_id") -] diff --git a/migrations/auth/20221117_02_fmuZh-create-group-users-table.py b/migrations/auth/20221117_02_fmuZh-create-group-users-table.py deleted file mode 100644 index 92885ef..0000000 --- a/migrations/auth/20221117_02_fmuZh-create-group-users-table.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -Create 'group_users' table. -""" - -from yoyo import step - -__depends__ = {'20221117_01_RDlfx-modify-group-roles-add-group-role-id'} - -steps = [ - step( - """ - CREATE TABLE IF NOT EXISTS group_users( - group_id TEXT NOT NULL, - user_id TEXT NOT NULL UNIQUE, -- user can only be in one group - PRIMARY KEY(group_id, user_id) - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS group_users"), - step( - """ - CREATE INDEX IF NOT EXISTS tbl_group_users_cols_group_id - ON group_users(group_id) - """, - "DROP INDEX IF EXISTS tbl_group_users_cols_group_id") -] diff --git a/migrations/auth/20221206_01_BbeF9-create-group-user-roles-on-resources-table.py b/migrations/auth/20221206_01_BbeF9-create-group-user-roles-on-resources-table.py deleted file mode 100644 index 9aa3667..0000000 --- a/migrations/auth/20221206_01_BbeF9-create-group-user-roles-on-resources-table.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -Create 'group_user_roles_on_resources' table -""" - -from yoyo import step - -__depends__ = {'20221117_02_fmuZh-create-group-users-table'} - -steps = [ - step( - """ - CREATE TABLE group_user_roles_on_resources ( - group_id TEXT NOT NULL, - user_id TEXT NOT NULL, - role_id TEXT NOT NULL, - resource_id TEXT NOT NULL, - PRIMARY KEY (group_id, user_id, role_id, resource_id), - FOREIGN KEY (user_id) - REFERENCES users(user_id) - ON UPDATE CASCADE ON DELETE RESTRICT, - FOREIGN KEY (group_id, role_id) - REFERENCES group_roles(group_id, role_id) - ON UPDATE CASCADE ON DELETE RESTRICT, - FOREIGN KEY (group_id, resource_id) - REFERENCES resources(group_id, resource_id) - ON UPDATE CASCADE ON DELETE RESTRICT - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS group_user_roles_on_resources"), - step( - """ - CREATE INDEX IF NOT EXISTS - idx_tbl_group_user_roles_on_resources_group_user_resource - ON group_user_roles_on_resources(group_id, user_id, resource_id) - """, - """ - DROP INDEX IF EXISTS - idx_tbl_group_user_roles_on_resources_group_user_resource""") -] diff --git a/migrations/auth/20221208_01_sSdHz-add-public-column-to-resources-table.py b/migrations/auth/20221208_01_sSdHz-add-public-column-to-resources-table.py deleted file mode 100644 index 2238069..0000000 --- a/migrations/auth/20221208_01_sSdHz-add-public-column-to-resources-table.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Add 'public' column to 'resources' table -""" - -from yoyo import step - -__depends__ = {'20221206_01_BbeF9-create-group-user-roles-on-resources-table'} - -steps = [ - step( - """ - ALTER TABLE resources ADD COLUMN - public INTEGER NOT NULL DEFAULT 0 CHECK (public=0 or public=1) - """, - "ALTER TABLE resources DROP COLUMN public") -] diff --git a/migrations/auth/20221219_01_CI3tN-create-oauth2-clients-table.py b/migrations/auth/20221219_01_CI3tN-create-oauth2-clients-table.py deleted file mode 100644 index 475be01..0000000 --- a/migrations/auth/20221219_01_CI3tN-create-oauth2-clients-table.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -create oauth2_clients table -""" - -from yoyo import step - -__depends__ = {'20221208_01_sSdHz-add-public-column-to-resources-table'} - -steps = [ - step( - """ - CREATE TABLE IF NOT EXISTS oauth2_clients( - client_id TEXT NOT NULL, - client_secret TEXT NOT NULL, - client_id_issued_at INTEGER NOT NULL, - client_secret_expires_at INTEGER NOT NULL, - client_metadata TEXT, - user_id TEXT NOT NULL, - PRIMARY KEY(client_id), - FOREIGN KEY(user_id) REFERENCES users(user_id) - ON UPDATE CASCADE ON DELETE RESTRICT - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS oauth2_clients") -] diff --git a/migrations/auth/20221219_02_buSEU-create-oauth2-tokens-table.py b/migrations/auth/20221219_02_buSEU-create-oauth2-tokens-table.py deleted file mode 100644 index 778282b..0000000 --- a/migrations/auth/20221219_02_buSEU-create-oauth2-tokens-table.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -create oauth2_tokens table -""" - -from yoyo import step - -__depends__ = {'20221219_01_CI3tN-create-oauth2-clients-table'} - -steps = [ - step( - """ - CREATE TABLE oauth2_tokens( - token_id TEXT NOT NULL, - client_id TEXT NOT NULL, - token_type TEXT NOT NULL, - access_token TEXT UNIQUE NOT NULL, - refresh_token TEXT, - scope TEXT, - revoked INTEGER CHECK (revoked = 0 or revoked = 1), - issued_at INTEGER NOT NULL, - expires_in INTEGER NOT NULL, - user_id TEXT NOT NULL, - PRIMARY KEY(token_id), - FOREIGN KEY (client_id) REFERENCES oauth2_clients(client_id) - ON UPDATE CASCADE ON DELETE RESTRICT, - FOREIGN KEY (user_id) REFERENCES users(user_id) - ON UPDATE CASCADE ON DELETE RESTRICT - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS oauth2_tokens") -] diff --git a/migrations/auth/20221219_03_PcTrb-create-authorisation-code-table.py b/migrations/auth/20221219_03_PcTrb-create-authorisation-code-table.py deleted file mode 100644 index 1683f87..0000000 --- a/migrations/auth/20221219_03_PcTrb-create-authorisation-code-table.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -create authorisation_code table -""" - -from yoyo import step - -__depends__ = {'20221219_02_buSEU-create-oauth2-tokens-table'} - -steps = [ - step( - """ - CREATE TABLE authorisation_code ( - code_id TEXT NOT NULL, - code TEXT UNIQUE NOT NULL, - client_id NOT NULL, - redirect_uri TEXT, - scope TEXT, - nonce TEXT, - auth_time INTEGER NOT NULL, - code_challenge TEXT, - code_challenge_method TEXT, - user_id TEXT NOT NULL, - PRIMARY KEY (code_id), - FOREIGN KEY (client_id) REFERENCES oauth2_clients(client_id) - ON UPDATE CASCADE ON DELETE RESTRICT, - FOREIGN KEY (user_id) REFERENCES users(user_id) - ON UPDATE CASCADE ON DELETE RESTRICT - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS authorisation_code") -] diff --git a/migrations/auth/20230111_01_Wd6IZ-remove-create-group-privilege-from-group-leader.py b/migrations/auth/20230111_01_Wd6IZ-remove-create-group-privilege-from-group-leader.py deleted file mode 100644 index 7e7fda2..0000000 --- a/migrations/auth/20230111_01_Wd6IZ-remove-create-group-privilege-from-group-leader.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -remove 'create-group' privilege from group-leader. -""" - -from yoyo import step - -__depends__ = {'20221219_03_PcTrb-create-authorisation-code-table'} - -steps = [ - step( - """ - DELETE FROM role_privileges - WHERE role_id='a0e67630-d502-4b9f-b23f-6805d0f30e30' - AND privilege_id='4842e2aa-38b9-4349-805e-0a99a9cf8bff' - """, - """ - INSERT INTO role_privileges VALUES - ('a0e67630-d502-4b9f-b23f-6805d0f30e30', - '4842e2aa-38b9-4349-805e-0a99a9cf8bff') - """), - step( - """ - INSERT INTO roles(role_id, role_name, user_editable) VALUES - ('ade7e6b0-ba9c-4b51-87d0-2af7fe39a347', 'group-creator', '0') - """, - """ - DELETE FROM roles WHERE role_id='ade7e6b0-ba9c-4b51-87d0-2af7fe39a347' - """), - step( - """ - INSERT INTO role_privileges VALUES - ('ade7e6b0-ba9c-4b51-87d0-2af7fe39a347', - '4842e2aa-38b9-4349-805e-0a99a9cf8bff') - """, - """ - DELETE FROM role_privileges - WHERE role_id='ade7e6b0-ba9c-4b51-87d0-2af7fe39a347' - AND privilege_id='4842e2aa-38b9-4349-805e-0a99a9cf8bff' - """) -] diff --git a/migrations/auth/20230116_01_KwuJ3-rework-privileges-schema.py b/migrations/auth/20230116_01_KwuJ3-rework-privileges-schema.py deleted file mode 100644 index 1ef5ab0..0000000 --- a/migrations/auth/20230116_01_KwuJ3-rework-privileges-schema.py +++ /dev/null @@ -1,111 +0,0 @@ -""" -rework privileges schema -""" -import contextlib - -from yoyo import step - -__depends__ = {'20230111_01_Wd6IZ-remove-create-group-privilege-from-group-leader'} - -privileges = ( # format: (original_id, original_name, new_id, category) - ("13ec2a94-4f1a-442d-aad2-936ad6dd5c57", "delete-group", - "system:group:delete-group", "group-management"), - ("1c59eff5-9336-4ed2-a166-8f70d4cb012e", "delete-role", - "group:role:delete-role", "role-management"), - ("1fe61370-cae9-4983-bd6c-ce61050c510f", "delete-any-user", - "system:user:delete-user", "user-management"), - ("221660b1-df05-4be1-b639-f010269dbda9", "create-role", - "group:role:create-role", "role-management"), - ("2f980855-959b-4339-b80e-25d1ec286e21", "edit-resource", - "group:resource:edit-resource", "resource-management"), - ("3ebfe79c-d159-4629-8b38-772cf4bc2261", "view-group", - "system:group:view-group", "group-management"), - ("4842e2aa-38b9-4349-805e-0a99a9cf8bff", "create-group", - "system:group:create-group", "group-management"), - ("5103cc68-96f8-4ebb-83a4-a31692402c9b", "assign-role", - "group:user:assign-role", "role-management"), - ("519db546-d44e-4fdc-9e4e-25aa67548ab3", "masquerade", - "system:user:masquerade", "system-admin"), - ("52576370-b3c7-4e6a-9f7e-90e9dbe24d8f", "edit-group", - "system:group:edit-group", "group-management"), - ("7bcca363-cba9-4169-9e31-26bdc6179b28", "edit-role", - "group:role:edit-role", "role-management"), - ("7f261757-3211-4f28-a43f-a09b800b164d", "view-resource", - "group:resource:view-resource", "resource-management"), - ("80f11285-5079-4ec0-907c-06509f88a364", "assign-group-leader", - "system:user:assign-group-leader", "group-management"), - ("aa25b32a-bff2-418d-b0a2-e26b4a8f089b", "create-resource", - "group:resource:create-resource", "resource-management"), - ("ae4add8c-789a-4d11-a6e9-a306470d83d9", "add-group-member", - "group:user:add-group-member", "group-management"), - ("d2a070fd-e031-42fb-ba41-d60cf19e5d6d", "delete-resource", - "group:resource:delete-resource", "resource-management"), - ("d4afe2b3-4ca0-4edd-b37d-966535b5e5bd", "transfer-group-leadership", - "system:group:transfer-group-leader", "group-management"), - ("e7252301-6ee0-43ba-93ef-73b607cf06f6", "reset-any-password", - "system:user:reset-password", "user-management"), - ("f1bd3f42-567e-4965-9643-6d1a52ddee64", "remove-group-member", - "group:user:remove-group-member", "group-management")) - -def rework_privileges_table(cursor): - "rework the schema" - cursor.executemany( - ("UPDATE privileges SET privilege_id=:id " - "WHERE privilege_id=:old_id"), - ({"id": row[2], "old_id": row[0]} for row in privileges)) - cursor.execute("ALTER TABLE privileges DROP COLUMN privilege_category") - cursor.execute("ALTER TABLE privileges DROP COLUMN privilege_name") - -def restore_privileges_table(cursor): - "restore the schema" - cursor.execute(( - "CREATE TABLE privileges_restore (" - " privilege_id TEXT PRIMARY KEY," - " privilege_name TEXT NOT NULL," - " privilege_category TEXT NOT NULL DEFAULT 'common'," - " privilege_description TEXT" - ")")) - id_dict = {row[2]: {"id": row[0], "name": row[1], "cat": row[3]} - for row in privileges} - cursor.execute( - "SELECT privilege_id, privilege_description FROM privileges") - params = ({**id_dict[row[0]], "desc": row[1]} for row in cursor.fetchall()) - cursor.executemany( - "INSERT INTO privileges_restore VALUES (:id, :name, :cat, :desc)", - params) - cursor.execute("DROP TABLE privileges") - cursor.execute("ALTER TABLE privileges_restore RENAME TO privileges") - -def update_privilege_ids_in_role_privileges(cursor): - """Update the ids to new form.""" - cursor.executemany( - ("UPDATE role_privileges SET privilege_id=:new_id " - "WHERE privilege_id=:old_id"), - ({"new_id": row[2], "old_id": row[0]} for row in privileges)) - -def restore_privilege_ids_in_role_privileges(cursor): - """Restore original ids""" - cursor.executemany( - ("UPDATE role_privileges SET privilege_id=:old_id " - "WHERE privilege_id=:new_id"), - ({"new_id": row[2], "old_id": row[0]} for row in privileges)) - -def change_schema(conn): - """Change the privileges schema and IDs""" - with contextlib.closing(conn.cursor()) as cursor: - cursor.execute("PRAGMA foreign_keys=OFF") - rework_privileges_table(cursor) - update_privilege_ids_in_role_privileges(cursor) - cursor.execute("PRAGMA foreign_keys=ON") - -def restore_schema(conn): - """Change the privileges schema and IDs""" - with contextlib.closing(conn.cursor()) as cursor: - cursor.execute("PRAGMA foreign_keys=OFF") - restore_privilege_ids_in_role_privileges(cursor) - restore_privileges_table(cursor) - cursor.execute("PRAGMA foreign_keys=ON") - -steps = [ - step(change_schema, restore_schema) -] diff --git a/migrations/auth/20230207_01_r0bkZ-create-group-join-requests-table.py b/migrations/auth/20230207_01_r0bkZ-create-group-join-requests-table.py deleted file mode 100644 index ceae5ea..0000000 --- a/migrations/auth/20230207_01_r0bkZ-create-group-join-requests-table.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Create group_requests table -""" - -from yoyo import step - -__depends__ = {'20230116_01_KwuJ3-rework-privileges-schema'} - -steps = [ - step( - """ - CREATE TABLE IF NOT EXISTS group_join_requests( - request_id TEXT NOT NULL, - group_id TEXT NOT NULL, - requester_id TEXT NOT NULL, - timestamp REAL NOT NULL, - status TEXT NOT NULL DEFAULT 'PENDING', - message TEXT, - PRIMARY KEY(request_id, group_id), - FOREIGN KEY(group_id) REFERENCES groups(group_id) - ON UPDATE CASCADE ON DELETE CASCADE, - FOREIGN KEY (requester_id) REFERENCES users(user_id) - ON UPDATE CASCADE ON DELETE CASCADE, - UNIQUE(group_id, requester_id), - CHECK (status IN ('PENDING', 'ACCEPTED', 'REJECTED')) - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS group_join_requests") -] diff --git a/migrations/auth/20230210_01_8xMa1-system-admin-privileges-for-data-distribution.py b/migrations/auth/20230210_01_8xMa1-system-admin-privileges-for-data-distribution.py deleted file mode 100644 index 8b406a6..0000000 --- a/migrations/auth/20230210_01_8xMa1-system-admin-privileges-for-data-distribution.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -System admin privileges for data distribution - -These privileges are focussed on allowing the system administrator to link the -datasets and traits in the main database to specific groups in the auth system. -""" - -from yoyo import step - -__depends__ = {'20230207_01_r0bkZ-create-group-join-requests-table'} - -steps = [ - step( - """ - INSERT INTO privileges VALUES - ('system:data:link-to-group', 'Link a dataset or trait to a group.') - """, - """ - DELETE FROM privileges WHERE privilege_id IN - ('system:data:link-to-group') - """) -] diff --git a/migrations/auth/20230210_02_lDK14-create-system-admin-role.py b/migrations/auth/20230210_02_lDK14-create-system-admin-role.py deleted file mode 100644 index 9b3fc2b..0000000 --- a/migrations/auth/20230210_02_lDK14-create-system-admin-role.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -Create system-admin role -""" -import uuid -from contextlib import closing - -from yoyo import step - -__depends__ = {'20230210_01_8xMa1-system-admin-privileges-for-data-distribution'} - -def create_sys_admin_role(conn): - with closing(conn.cursor()) as cursor: - role_id = uuid.uuid4() - cursor.execute( - "INSERT INTO roles VALUES (?, 'system-administrator', '0')", - (str(role_id),)) - - cursor.executemany( - "INSERT INTO role_privileges VALUES (:role_id, :privilege_id)", - ({"role_id": f"{role_id}", "privilege_id": priv} - for priv in ( - "system:data:link-to-group", - "system:group:create-group", - "system:group:delete-group", - "system:group:edit-group", - "system:group:transfer-group-leader", - "system:group:view-group", - "system:user:assign-group-leader", - "system:user:delete-user", - "system:user:masquerade", - "system:user:reset-password"))) - -def drop_sys_admin_role(conn): - pass - -steps = [ - step(create_sys_admin_role, drop_sys_admin_role) -] diff --git a/migrations/auth/20230306_01_pRfxl-add-system-user-list-privilege.py b/migrations/auth/20230306_01_pRfxl-add-system-user-list-privilege.py deleted file mode 100644 index 84bbd49..0000000 --- a/migrations/auth/20230306_01_pRfxl-add-system-user-list-privilege.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Add system:user:list privilege -""" -import contextlib - -from yoyo import step - -__depends__ = {'20230210_02_lDK14-create-system-admin-role'} - -def insert_users_list_priv(conn): - """Create a new 'system:user:list' privilege.""" - with contextlib.closing(conn.cursor()) as cursor: - cursor.execute( - "INSERT INTO privileges(privilege_id, privilege_description) " - "VALUES('system:user:list', 'List users in the system') " - "ON CONFLICT (privilege_id) DO NOTHING") - -def delete_users_list_priv(conn): - """Delete the new 'system:user:list' privilege.""" - with contextlib.closing(conn.cursor()) as cursor: - cursor.execute( - "DELETE FROM privileges WHERE privilege_id='system:user:list'") - -steps = [ - step(insert_users_list_priv, delete_users_list_priv) -] diff --git a/migrations/auth/20230306_02_7GnRY-add-system-user-list-privilege-to-system-administrator-and-group-leader-roles.py b/migrations/auth/20230306_02_7GnRY-add-system-user-list-privilege-to-system-administrator-and-group-leader-roles.py deleted file mode 100644 index 3caad55..0000000 --- a/migrations/auth/20230306_02_7GnRY-add-system-user-list-privilege-to-system-administrator-and-group-leader-roles.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -Add system:user:list privilege to system-administrator and group-leader roles. -""" -import uuid -import contextlib - -from yoyo import step - -__depends__ = {'20230306_01_pRfxl-add-system-user-list-privilege'} - -def role_ids(cursor): - """Get role ids from names""" - cursor.execute( - "SELECT * FROM roles WHERE role_name IN " - "('system-administrator', 'group-leader')") - return (uuid.UUID(row[0]) for row in cursor.fetchall()) - -def add_privilege_to_roles(conn): - """ - Add 'system:user:list' privilege to 'system-administrator' and - 'group-leader' roles.""" - with contextlib.closing(conn.cursor()) as cursor: - cursor.executemany( - "INSERT INTO role_privileges(role_id,privilege_id) " - "VALUES(?, ?)", - tuple((str(role_id), "system:user:list") - for role_id in role_ids(cursor))) - -def del_privilege_from_roles(conn): - """ - Delete 'system:user:list' privilege to 'system-administrator' and - 'group-leader' roles. - """ - with contextlib.closing(conn.cursor()) as cursor: - cursor.execute( - "DELETE FROM role_privileges WHERE " - "role_id IN (?, ?) AND privilege_id='system:user:list'", - tuple(str(role_id) for role_id in role_ids(cursor))) - -steps = [ - step(add_privilege_to_roles, del_privilege_from_roles) -] diff --git a/migrations/auth/20230322_01_0dDZR-create-linked-phenotype-data-table.py b/migrations/auth/20230322_01_0dDZR-create-linked-phenotype-data-table.py deleted file mode 100644 index 647325f..0000000 --- a/migrations/auth/20230322_01_0dDZR-create-linked-phenotype-data-table.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -Create linked-phenotype-data table -""" - -from yoyo import step - -__depends__ = {'20230306_02_7GnRY-add-system-user-list-privilege-to-system-administrator-and-group-leader-roles'} - -steps = [ - step( - """ - CREATE TABLE IF NOT EXISTS linked_phenotype_data - -- Link the data in MariaDB to user groups in the auth system - ( - data_link_id TEXT NOT NULL PRIMARY KEY, -- A new ID for the auth system - group_id TEXT NOT NULL, -- The user group the data is linked to - SpeciesId TEXT NOT NULL, -- The species in MariaDB - InbredSetId TEXT NOT NULL, -- The traits group in MariaDB - PublishFreezeId TEXT NOT NULL, -- The dataset Id in MariaDB - dataset_name TEXT, -- dataset Name in MariaDB - dataset_fullname, -- dataset FullName in MariaDB - dataset_shortname, -- dataset ShortName in MariaDB - PublishXRefId TEXT NOT NULL, -- The trait's ID in MariaDB - FOREIGN KEY (group_id) - REFERENCES groups(group_id) ON UPDATE CASCADE ON DELETE RESTRICT - UNIQUE (SpeciesId, InbredSetId, PublishFreezeId, PublishXRefId) - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS linked_phenotype_data") -] diff --git a/migrations/auth/20230322_02_Ll854-create-phenotype-resources-table.py b/migrations/auth/20230322_02_Ll854-create-phenotype-resources-table.py deleted file mode 100644 index 7c9e986..0000000 --- a/migrations/auth/20230322_02_Ll854-create-phenotype-resources-table.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Create phenotype_resources table -""" - -from yoyo import step - -__depends__ = {'20230322_01_0dDZR-create-linked-phenotype-data-table'} - -steps = [ - step( - """ - CREATE TABLE IF NOT EXISTS phenotype_resources - -- Link phenotype data to specific resources - ( - group_id TEXT NOT NULL, - resource_id TEXT NOT NULL, -- A resource can have multiple data items - data_link_id TEXT NOT NULL, - PRIMARY KEY(group_id, resource_id, data_link_id), - UNIQUE (data_link_id), -- ensure data is linked to only one resource - FOREIGN KEY (group_id, resource_id) - REFERENCES resources(group_id, resource_id) - ON UPDATE CASCADE ON DELETE RESTRICT, - FOREIGN KEY (data_link_id) - REFERENCES linked_phenotype_data(data_link_id) - ON UPDATE CASCADE ON DELETE RESTRICT - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS phenotype_resources") -] diff --git a/migrations/auth/20230404_01_VKxXg-create-linked-genotype-data-table.py b/migrations/auth/20230404_01_VKxXg-create-linked-genotype-data-table.py deleted file mode 100644 index 02e8718..0000000 --- a/migrations/auth/20230404_01_VKxXg-create-linked-genotype-data-table.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Create linked genotype data table -""" - -from yoyo import step - -__depends__ = {'20230322_02_Ll854-create-phenotype-resources-table'} - -steps = [ - step( - """ - CREATE TABLE IF NOT EXISTS linked_genotype_data - -- Link genotype data in MariaDB to user groups in auth system - ( - data_link_id TEXT NOT NULL PRIMARY KEY, -- A new ID for the auth system - group_id TEXT NOT NULL, -- The user group the data is linked to - SpeciesId TEXT NOT NULL, -- The species in MariaDB - InbredSetId TEXT NOT NULL, -- The traits group in MariaDB - GenoFreezeId TEXT NOT NULL, -- The dataset Id in MariaDB - dataset_name TEXT, -- dataset Name in MariaDB - dataset_fullname, -- dataset FullName in MariaDB - dataset_shortname, -- dataset ShortName in MariaDB - FOREIGN KEY (group_id) - REFERENCES groups(group_id) ON UPDATE CASCADE ON DELETE RESTRICT - UNIQUE (SpeciesId, InbredSetId, GenoFreezeId) - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS linked_genotype_data") -] diff --git a/migrations/auth/20230404_02_la33P-create-genotype-resources-table.py b/migrations/auth/20230404_02_la33P-create-genotype-resources-table.py deleted file mode 100644 index 1a865e0..0000000 --- a/migrations/auth/20230404_02_la33P-create-genotype-resources-table.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Create genotype resources table -""" - -from yoyo import step - -__depends__ = {'20230404_01_VKxXg-create-linked-genotype-data-table'} - -steps = [ - step( - """ - CREATE TABLE IF NOT EXISTS genotype_resources - -- Link genotype data to specific resource - ( - group_id TEXT NOT NULL, - resource_id TEXT NOT NULL, -- A resource can have multiple items - data_link_id TEXT NOT NULL, - PRIMARY KEY (group_id, resource_id, data_link_id), - UNIQUE (data_link_id) -- ensure data is linked to single resource - FOREIGN KEY (group_id, resource_id) - REFERENCES resources(group_id, resource_id) - ON UPDATE CASCADE ON DELETE RESTRICT, - FOREIGN KEY (data_link_id) - REFERENCES linked_genotype_data(data_link_id) - ON UPDATE CASCADE ON DELETE RESTRICT - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS genotype_resources") -] diff --git a/migrations/auth/20230410_01_8mwaf-create-linked-mrna-data-table.py b/migrations/auth/20230410_01_8mwaf-create-linked-mrna-data-table.py deleted file mode 100644 index db9a6bf..0000000 --- a/migrations/auth/20230410_01_8mwaf-create-linked-mrna-data-table.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -Create linked mrna data table -""" - -from yoyo import step - -__depends__ = {'20230404_02_la33P-create-genotype-resources-table'} - -steps = [ - step( - """ - CREATE TABLE IF NOT EXISTS linked_mrna_data - -- Link mRNA Assay data in MariaDB to user groups in auth system - ( - data_link_id TEXT NOT NULL PRIMARY KEY, -- A new ID for the auth system - group_id TEXT NOT NULL, -- The user group the data is linked to - SpeciesId TEXT NOT NULL, -- The species in MariaDB - InbredSetId TEXT NOT NULL, -- The traits group in MariaDB - ProbeFreezeId TEXT NOT NULL, -- The study ID in MariaDB - ProbeSetFreezeId TEXT NOT NULL, -- The dataset Id in MariaDB - dataset_name TEXT, -- dataset Name in MariaDB - dataset_fullname, -- dataset FullName in MariaDB - dataset_shortname, -- dataset ShortName in MariaDB - FOREIGN KEY (group_id) - REFERENCES groups(group_id) ON UPDATE CASCADE ON DELETE RESTRICT - UNIQUE (SpeciesId, InbredSetId, ProbeFreezeId, ProbeSetFreezeId) - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS linked_mrna_data") -] diff --git a/migrations/auth/20230410_02_WZqSf-create-mrna-resources-table.py b/migrations/auth/20230410_02_WZqSf-create-mrna-resources-table.py deleted file mode 100644 index 2ad1056..0000000 --- a/migrations/auth/20230410_02_WZqSf-create-mrna-resources-table.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -Create mRNA resources table -""" - -from yoyo import step - -__depends__ = {'20230410_01_8mwaf-create-linked-mrna-data-table'} - -steps = [ - step( - """ - CREATE TABLE IF NOT EXISTS mrna_resources - -- Link mRNA data to specific resource - ( - group_id TEXT NOT NULL, - resource_id TEXT NOT NULL, -- A resource can have multiple items - data_link_id TEXT NOT NULL, - PRIMARY KEY (resource_id, data_link_id), - UNIQUE (data_link_id) -- ensure data is linked to single resource - FOREIGN KEY (group_id, resource_id) - REFERENCES resources(group_id, resource_id) - ON UPDATE CASCADE ON DELETE RESTRICT, - FOREIGN KEY (data_link_id) REFERENCES linked_mrna_data(data_link_id) - ON UPDATE CASCADE ON DELETE RESTRICT - ) WITHOUT ROWID - """, - "DROP TABLE IF EXISTS mrna_resources") -] diff --git a/scripts/migrate_existing_data.py b/scripts/migrate_existing_data.py deleted file mode 100644 index 186f7f8..0000000 --- a/scripts/migrate_existing_data.py +++ /dev/null @@ -1,381 +0,0 @@ -""" -Migrate existing data that is not assigned to any group to the default sys-admin -group for accessibility purposes. -""" -import sys -import json -import time -import random -from pathlib import Path -from uuid import UUID, uuid4 - -import click -from MySQLdb.cursors import DictCursor - -from gn3 import db_utils as biodb - -from gn3.auth import db as authdb -from gn3.auth.authentication.users import User -from gn3.auth.authorisation.groups.models import Group, save_group -from gn3.auth.authorisation.roles.models import ( - revoke_user_role_by_name, assign_user_role_by_name) -from gn3.auth.authorisation.resources.models import ( - Resource, ResourceCategory, __assign_resource_owner_role__) - -class DataNotFound(Exception): - """Raise if no admin user exists.""" - -def sys_admins(conn: authdb.DbConnection) -> tuple[User, ...]: - """Retrieve all the existing system admins.""" - with authdb.cursor(conn) as cursor: - cursor.execute( - "SELECT u.* FROM users AS u " - "INNER JOIN user_roles AS ur ON u.user_id=ur.user_id " - "INNER JOIN roles AS r ON ur.role_id=r.role_id " - "WHERE r.role_name='system-administrator'") - return tuple(User(UUID(row["user_id"]), row["email"], row["name"]) - for row in cursor.fetchall()) - return tuple() - -def choose_admin(enum_admins: dict[int, User]) -> int: - """Prompt and read user choice.""" - while True: - try: - print("\n===========================\n") - print("We found the following system administrators:") - for idx, admin in enum_admins.items(): - print(f"\t{idx}: {admin.name} ({admin.email})") - choice = input(f"Choose [1 .. {len(enum_admins)}]: ") - return int(choice) - except ValueError as _verr: - if choice.lower() == "quit": - print("Goodbye!") - sys.exit(0) - print(f"\nERROR: Invalid choice '{choice}'!") - -def select_sys_admin(admins: tuple[User, ...]) -> User: - """Pick one admin out of list.""" - if len(admins) > 0: - if len(admins) == 1: - print(f"-> Found Admin: {admins[0].name} ({admins[0].email})") - return admins[0] - enum_admins = dict(enumerate(admins, start=1)) - chosen = enum_admins[choose_admin(enum_admins)] - print(f"-> Chosen Admin: {chosen.name} ({chosen.email})") - return chosen - raise DataNotFound( - "No administrator user found. Create an administrator user first.") - -def admin_group(conn: authdb.DbConnection, admin: User) -> Group: - """Retrieve the admin's user group. If none exist, create one.""" - with authdb.cursor(conn) as cursor: - cursor.execute( - "SELECT g.* FROM users AS u " - "INNER JOIN group_users AS gu ON u.user_id=gu.user_id " - "INNER JOIN groups AS g on gu.group_id=g.group_id " - "WHERE u.user_id = ?", - (str(admin.user_id),)) - row = cursor.fetchone() - if row: - return Group(UUID(row["group_id"]), - row["group_name"], - json.loads(row["group_metadata"])) - new_group = save_group(cursor, "AutoAdminGroup", { - "group_description": ( - "Created by script for existing data visibility. " - "Existing data was migrated into this group and assigned " - "to publicly visible resources according to type.") - }) - cursor.execute("INSERT INTO group_users VALUES (?, ?)", - (str(new_group.group_id), str(admin.user_id))) - revoke_user_role_by_name(cursor, admin, "group-creator") - assign_user_role_by_name(cursor, admin, "group-leader") - return new_group - -def __resource_category_by_key__( - cursor: authdb.DbCursor, category_key: str) -> ResourceCategory: - """Retrieve a resource category by its ID.""" - cursor.execute( - "SELECT * FROM resource_categories WHERE resource_category_key = ?", - (category_key,)) - row = cursor.fetchone() - if not bool(row): - raise DataNotFound( - f"Could not find resource category with key {category_key}") - return ResourceCategory(UUID(row["resource_category_id"]), - row["resource_category_key"], - row["resource_category_description"]) - -def __create_resources__(cursor: authdb.DbCursor, group: Group) -> tuple[ - Resource, ...]: - """Create default resources.""" - resources = tuple(Resource( - group, uuid4(), name, __resource_category_by_key__(cursor, catkey), - True, tuple() - ) for name, catkey in ( - ("mRNA-euhrin", "mrna"), - ("pheno-xboecp", "phenotype"), - ("geno-welphd", "genotype"))) - cursor.executemany( - "INSERT INTO resources VALUES (:gid, :rid, :rname, :rcid, :pub)", - tuple({ - "gid": str(group.group_id), - "rid": str(res.resource_id), - "rname": res.resource_name, - "rcid": str(res.resource_category.resource_category_id), - "pub": 1 - } for res in resources)) - return resources - -def default_resources(conn: authdb.DbConnection, group: Group) -> tuple[ - Resource, ...]: - """Create default resources, or return them if they exist.""" - with authdb.cursor(conn) as cursor: - cursor.execute( - "SELECT r.resource_id, r.resource_name, r.public, rc.* " - "FROM resources AS r INNER JOIN resource_categories AS rc " - "ON r.resource_category_id=rc.resource_category_id " - "WHERE r.group_id=? AND r.resource_name IN " - "('mRNA-euhrin', 'pheno-xboecp', 'geno-welphd')", - (str(group.group_id),)) - rows = cursor.fetchall() - if len(rows) == 0: - return __create_resources__(cursor, group) - - return tuple(Resource( - group, - UUID(row["resource_id"]), - row["resource_name"], - ResourceCategory( - UUID(row["resource_category_id"]), - row["resource_category_key"], - row["resource_category_description"]), - bool(row["public"]), - tuple() - ) for row in rows) - -def delay(): - """Delay a while: anything from 2 seconds to 15 seconds.""" - time.sleep(random.choice(range(2,16))) - -def __assigned_mrna__(authconn): - """Retrieve assigned mRNA items.""" - with authdb.cursor(authconn) as cursor: - cursor.execute( - "SELECT SpeciesId, InbredSetId, ProbeFreezeId, ProbeSetFreezeId " - "FROM linked_mrna_data") - return tuple( - (row["SpeciesId"], row["InbredSetId"], row["ProbeFreezeId"], - row["ProbeSetFreezeId"]) for row in cursor.fetchall()) - -def __unassigned_mrna__(bioconn, assigned): - """Retrieve unassigned mRNA data items.""" - query = ( - "SELECT s.SpeciesId, iset.InbredSetId, pf.ProbeFreezeId, " - "psf.Id AS ProbeSetFreezeId, psf.Name AS dataset_name, " - "psf.FullName AS dataset_fullname, psf.ShortName AS dataset_shortname " - "FROM Species AS s INNER JOIN InbredSet AS iset " - "ON s.SpeciesId=iset.SpeciesId INNER JOIN ProbeFreeze AS pf " - "ON iset.InbredSetId=pf.InbredSetId INNER JOIN ProbeSetFreeze AS psf " - "ON pf.ProbeFreezeId=psf.ProbeFreezeId ") - if len(assigned) > 0: - paramstr = ", ".join(["(%s, %s, %s, %s)"] * len(assigned)) - query = query + ( - "WHERE (s.SpeciesId, iset.InbredSetId, pf.ProbeFreezeId, psf.Id) " - f"NOT IN ({paramstr}) ") - - query = query + "LIMIT 100000" - with bioconn.cursor(DictCursor) as cursor: - cursor.execute(query, tuple(item for row in assigned for item in row)) - return (row for row in cursor.fetchall()) - -def __assign_mrna__(authconn, bioconn, resource): - "Assign any unassigned mRNA data to resource." - while True: - unassigned = tuple({ - "data_link_id": str(uuid4()), - "group_id": str(resource.group.group_id), - "resource_id": str(resource.resource_id), - **row - } for row in __unassigned_mrna__( - bioconn, __assigned_mrna__(authconn))) - - if len(unassigned) <= 0: - print("-> mRNA: Completed!") - break - with authdb.cursor(authconn) as cursor: - cursor.executemany( - "INSERT INTO linked_mrna_data VALUES " - "(:data_link_id, :group_id, :SpeciesId, :InbredSetId, " - ":ProbeFreezeId, :ProbeSetFreezeId, :dataset_name, " - ":dataset_fullname, :dataset_shortname)", - unassigned) - cursor.executemany( - "INSERT INTO mrna_resources VALUES " - "(:group_id, :resource_id, :data_link_id)", - unassigned) - print(f"-> mRNA: Linked {len(unassigned)}") - delay() - -def __assigned_geno__(authconn): - """Retrieve assigned genotype data.""" - with authdb.cursor(authconn) as cursor: - cursor.execute( - "SELECT SpeciesId, InbredSetId, GenoFreezeId " - "FROM linked_genotype_data") - return tuple((row["SpeciesId"], row["InbredSetId"], row["GenoFreezeId"]) - for row in cursor.fetchall()) - -def __unassigned_geno__(bioconn, assigned): - """Fetch unassigned genotype data.""" - query = ( - "SELECT s.SpeciesId, iset.InbredSetId, iset.InbredSetName, " - "gf.Id AS GenoFreezeId, gf.Name AS dataset_name, " - "gf.FullName AS dataset_fullname, " - "gf.ShortName AS dataset_shortname " - "FROM Species AS s INNER JOIN InbredSet AS iset " - "ON s.SpeciesId=iset.SpeciesId INNER JOIN GenoFreeze AS gf " - "ON iset.InbredSetId=gf.InbredSetId ") - if len(assigned) > 0: - paramstr = ", ".join(["(%s, %s, %s)"] * len(assigned)) - query = query + ( - "WHERE (s.SpeciesId, iset.InbredSetId, gf.Id) " - f"NOT IN ({paramstr}) ") - - query = query + "LIMIT 100000" - with bioconn.cursor(DictCursor) as cursor: - cursor.execute(query, tuple(item for row in assigned for item in row)) - return (row for row in cursor.fetchall()) - -def __assign_geno__(authconn, bioconn, resource): - "Assign any unassigned Genotype data to resource." - while True: - unassigned = tuple({ - "data_link_id": str(uuid4()), - "group_id": str(resource.group.group_id), - "resource_id": str(resource.resource_id), - **row - } for row in __unassigned_geno__( - bioconn, __assigned_geno__(authconn))) - - if len(unassigned) <= 0: - print("-> Genotype: Completed!") - break - with authdb.cursor(authconn) as cursor: - cursor.executemany( - "INSERT INTO linked_genotype_data VALUES " - "(:data_link_id, :group_id, :SpeciesId, :InbredSetId, " - ":GenoFreezeId, :dataset_name, :dataset_fullname, " - ":dataset_shortname)", - unassigned) - cursor.executemany( - "INSERT INTO genotype_resources VALUES " - "(:group_id, :resource_id, :data_link_id)", - unassigned) - print(f"-> Genotype: Linked {len(unassigned)}") - delay() - -def __assigned_pheno__(authconn): - """Retrieve assigned phenotype data.""" - with authdb.cursor(authconn) as cursor: - cursor.execute( - "SELECT SpeciesId, InbredSetId, PublishFreezeId, PublishXRefId " - "FROM linked_phenotype_data") - return tuple(( - row["SpeciesId"], row["InbredSetId"], row["PublishFreezeId"], - row["PublishXRefId"]) for row in cursor.fetchall()) - -def __unassigned_pheno__(bioconn, assigned): - """Retrieve all unassigned Phenotype data.""" - query = ( - "SELECT spc.SpeciesId, iset.InbredSetId, " - "pf.Id AS PublishFreezeId, pf.Name AS dataset_name, " - "pf.FullName AS dataset_fullname, " - "pf.ShortName AS dataset_shortname, pxr.Id AS PublishXRefId " - "FROM " - "Species AS spc " - "INNER JOIN InbredSet AS iset " - "ON spc.SpeciesId=iset.SpeciesId " - "INNER JOIN PublishFreeze AS pf " - "ON iset.InbredSetId=pf.InbredSetId " - "INNER JOIN PublishXRef AS pxr " - "ON pf.InbredSetId=pxr.InbredSetId ") - if len(assigned) > 0: - paramstr = ", ".join(["(%s, %s, %s, %s)"] * len(assigned)) - query = query + ( - "WHERE (spc.SpeciesId, iset.InbredSetId, pf.Id, pxr.Id) " - f"NOT IN ({paramstr}) ") - - query = query + "LIMIT 100000" - with bioconn.cursor(DictCursor) as cursor: - cursor.execute(query, tuple(item for row in assigned for item in row)) - return (row for row in cursor.fetchall()) - -def __assign_pheno__(authconn, bioconn, resource): - """Assign any unassigned Phenotype data to resource.""" - while True: - unassigned = tuple({ - "data_link_id": str(uuid4()), - "group_id": str(resource.group.group_id), - "resource_id": str(resource.resource_id), - **row - } for row in __unassigned_pheno__( - bioconn, __assigned_pheno__(authconn))) - - if len(unassigned) <= 0: - print("-> Phenotype: Completed!") - break - with authdb.cursor(authconn) as cursor: - cursor.executemany( - "INSERT INTO linked_phenotype_data VALUES " - "(:data_link_id, :group_id, :SpeciesId, :InbredSetId, " - ":PublishFreezeId, :dataset_name, :dataset_fullname, " - ":dataset_shortname, :PublishXRefId)", - unassigned) - cursor.executemany( - "INSERT INTO phenotype_resources VALUES " - "(:group_id, :resource_id, :data_link_id)", - unassigned) - print(f"-> Phenotype: Linked {len(unassigned)}") - delay() - -def assign_data_to_resource(authconn, bioconn, resource: Resource): - """Assign existing data, not linked to any group to the resource.""" - assigner_fns = { - "mrna": __assign_mrna__, - "genotype": __assign_geno__, - "phenotype": __assign_pheno__ - } - return assigner_fns[resource.resource_category.resource_category_key]( - authconn, bioconn, resource) - -def entry(authdbpath, mysqldburi): - """Entry-point for data migration.""" - if not Path(authdbpath).exists(): - print( - f"ERROR: Auth db file `{authdbpath}` does not exist.", - file=sys.stderr) - sys.exit(2) - try: - with (authdb.connection(authdbpath) as authconn, - biodb.database_connection(mysqldburi) as bioconn): - admin = select_sys_admin(sys_admins(authconn)) - resources = default_resources( - authconn, admin_group(authconn, admin)) - for resource in resources: - assign_data_to_resource(authconn, bioconn, resource) - with authdb.cursor(authconn) as cursor: - __assign_resource_owner_role__(cursor, resource, admin) - except DataNotFound as dnf: - print(dnf.args[0], file=sys.stderr) - sys.exit(1) - -@click.command() -@click.argument("authdbpath") # "Path to the Auth(entic|oris)ation database" -@click.argument("mysqldburi") # "URI to the MySQL database with the biology data" -def run(authdbpath, mysqldburi): - """Setup command-line arguments.""" - entry(authdbpath, mysqldburi) - -if __name__ == "__main__": - run() # pylint: disable=[no-value-for-parameter] diff --git a/scripts/register_sys_admin.py b/scripts/register_sys_admin.py deleted file mode 100644 index 1696adb..0000000 --- a/scripts/register_sys_admin.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Script to register and mark a user account as sysadmin.""" -import sys -import uuid -import getpass -from pathlib import Path - -import click -from email_validator import validate_email, EmailNotValidError - -from gn3.auth import db -from gn3.auth.authentication.users import hash_password - -def fetch_email() -> str: - """Prompt user for email.""" - while True: - try: - user_input = input("Enter the administrator's email: ") - email = validate_email(user_input.strip(), check_deliverability=True) - return email["email"] - except EmailNotValidError as _enve: - print("You did not provide a valid email address. Try again...", - file=sys.stderr) - -def fetch_password() -> str: - """Prompt user for password.""" - while True: - passwd = getpass.getpass(prompt="Enter password: ").strip() - passwd2 = getpass.getpass(prompt="Confirm password: ").strip() - if passwd != "" and passwd == passwd2: - return passwd - if passwd == "": - print("Empty password not accepted", file=sys.stderr) - continue - if passwd != passwd2: - print("Passwords *MUST* match", file=sys.stderr) - continue - -def fetch_name() -> str: - """Prompt user for name""" - while True: - name = input("Enter the user's name: ").strip() - if name == "": - print("Invalid name.") - continue - return name - -def save_admin(conn: db.DbConnection, name: str, email: str, passwd: str): - """Save the details to the database and assign the new user as admin.""" - admin_id = uuid.uuid4() - admin = { - "user_id": str(admin_id), - "email": email, - "name": name, - "hash": hash_password(passwd) - } - with db.cursor(conn) as cursor: - cursor.execute("INSERT INTO users VALUES (:user_id, :email, :name)", - admin) - cursor.execute("INSERT INTO user_credentials VALUES (:user_id, :hash)", - admin) - cursor.execute( - "SELECT * FROM roles WHERE role_name='system-administrator'") - admin_role = cursor.fetchone() - cursor.execute("INSERT INTO user_roles VALUES (:user_id, :role_id)", - {**admin, "role_id": admin_role["role_id"]}) - return 0 - -def register_admin(authdbpath: Path): - """Register a user as a system admin.""" - assert authdbpath.exists(), "Could not find database file." - with db.connection(str(authdbpath)) as conn: - return save_admin(conn, fetch_name(), fetch_email(), fetch_password()) - -if __name__ == "__main__": - @click.command() - @click.argument("authdbpath") - def run(authdbpath): - """Entry-point for when script is run directly""" - return register_admin(Path(authdbpath).absolute()) - - run()# pylint: disable=[no-value-for-parameter] diff --git a/tests/unit/auth/__init__.py b/tests/unit/auth/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/tests/unit/auth/__init__.py +++ /dev/null diff --git a/tests/unit/auth/conftest.py b/tests/unit/auth/conftest.py deleted file mode 100644 index a7c64a8..0000000 --- a/tests/unit/auth/conftest.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Module for fixtures and test utilities""" -import uuid -import datetime -from contextlib import contextmanager - -from gn3.auth.authentication.oauth2.models.oauth2token import OAuth2Token - -from .fixtures import * # pylint: disable=[wildcard-import,unused-wildcard-import] - -def get_tokeniser(user): - """Get contextmanager for mocking token acquisition.""" - @contextmanager - def __token__(*args, **kwargs):# pylint: disable=[unused-argument] - yield { - usr.user_id: OAuth2Token( - token_id=uuid.UUID("d32611e3-07fc-4564-b56c-786c6db6de2b"), - client=None, token_type="Bearer", access_token="123456ABCDE", - refresh_token=None, revoked=False, expires_in=864000, - user=usr, issued_at=int(datetime.datetime.now().timestamp()), - scope="profile group role resource register-client") - for usr in TEST_USERS - }[user.user_id] - - return __token__ diff --git a/tests/unit/auth/fixtures/__init__.py b/tests/unit/auth/fixtures/__init__.py deleted file mode 100644 index a675fc7..0000000 --- a/tests/unit/auth/fixtures/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -"""pytest's conftest as a module.""" -from .role_fixtures import * -from .user_fixtures import * -from .group_fixtures import * -from .resource_fixtures import * -# from .privilege_fixtures import * -from .migration_fixtures import * -from .oauth2_client_fixtures import * diff --git a/tests/unit/auth/fixtures/group_fixtures.py b/tests/unit/auth/fixtures/group_fixtures.py deleted file mode 100644 index d7bbc56..0000000 --- a/tests/unit/auth/fixtures/group_fixtures.py +++ /dev/null @@ -1,147 +0,0 @@ -"""Fixtures and utilities for group-related tests""" -import uuid - -import pytest - -from gn3.auth import db -from gn3.auth.authorisation.groups import Group, GroupRole -from gn3.auth.authorisation.resources import Resource, ResourceCategory - -from .role_fixtures import RESOURCE_EDITOR_ROLE, RESOURCE_READER_ROLE - -TEST_GROUP_01 = Group(uuid.UUID("9988c21d-f02f-4d45-8966-22c968ac2fbf"), - "TheTestGroup", {}) -TEST_GROUP_02 = Group(uuid.UUID("e37d59d7-c05e-4d67-b479-81e627d8d634"), - "AnotherTestGroup", {}) -TEST_GROUPS = (TEST_GROUP_01, TEST_GROUP_02) - -TEST_RESOURCES_GROUP_01 = ( - Resource(TEST_GROUPS[0], uuid.UUID("26ad1668-29f5-439d-b905-84d551f85955"), - "ResourceG01R01", - ResourceCategory(uuid.UUID("48056f84-a2a6-41ac-8319-0e1e212cba2a"), - "genotype", "Genotype Dataset"), - True), - Resource(TEST_GROUPS[0], uuid.UUID("2130aec0-fefd-434d-92fd-9ca342348b2d"), - "ResourceG01R02", - ResourceCategory(uuid.UUID("548d684b-d4d1-46fb-a6d3-51a56b7da1b3"), - "phenotype", "Phenotype (Publish) Dataset"), - False), - Resource(TEST_GROUPS[0], uuid.UUID("e9a1184a-e8b4-49fb-b713-8d9cbeea5b83"), - "ResourceG01R03", - ResourceCategory(uuid.UUID("fad071a3-2fc8-40b8-992b-cdefe7dcac79"), - "mrna", "mRNA Dataset"), - False)) - -TEST_RESOURCES_GROUP_02 = ( - Resource(TEST_GROUPS[1], uuid.UUID("14496a1c-c234-49a2-978c-8859ea274054"), - "ResourceG02R01", - ResourceCategory(uuid.UUID("48056f84-a2a6-41ac-8319-0e1e212cba2a"), - "genotype", "Genotype Dataset"), - False), - Resource(TEST_GROUPS[1], uuid.UUID("04ad9e09-94ea-4390-8a02-11f92999806b"), - "ResourceG02R02", - ResourceCategory(uuid.UUID("fad071a3-2fc8-40b8-992b-cdefe7dcac79"), - "mrna", "mRNA Dataset"), - True)) - -TEST_RESOURCES = TEST_RESOURCES_GROUP_01 + TEST_RESOURCES_GROUP_02 -TEST_RESOURCES_PUBLIC = (TEST_RESOURCES_GROUP_01[0], TEST_RESOURCES_GROUP_02[1]) - -def __gtuple__(cursor): - return tuple(dict(row) for row in cursor.fetchall()) - -@pytest.fixture(scope="function") -def fxtr_group(conn_after_auth_migrations):# pylint: disable=[redefined-outer-name] - """Fixture: setup a test group.""" - query = "INSERT INTO groups(group_id, group_name) VALUES (?, ?)" - with db.cursor(conn_after_auth_migrations) as cursor: - cursor.executemany( - query, tuple( - (str(group.group_id), group.group_name) - for group in TEST_GROUPS)) - - yield (conn_after_auth_migrations, TEST_GROUPS[0]) - - with db.cursor(conn_after_auth_migrations) as cursor: - cursor.executemany( - "DELETE FROM groups WHERE group_id=?", - ((str(group.group_id),) for group in TEST_GROUPS)) - -@pytest.fixture(scope="function") -def fxtr_users_in_group(fxtr_group, fxtr_users):# pylint: disable=[redefined-outer-name, unused-argument] - """Link the users to the groups.""" - conn, all_users = fxtr_users - users = tuple( - user for user in all_users if user.email not in ("unaff@iliated.user",)) - query_params = tuple( - (str(TEST_GROUP_01.group_id), str(user.user_id)) for user in users) - with db.cursor(conn) as cursor: - cursor.executemany( - "INSERT INTO group_users(group_id, user_id) VALUES (?, ?)", - query_params) - - yield (conn, TEST_GROUP_01, users) - - with db.cursor(conn) as cursor: - cursor.executemany( - "DELETE FROM group_users WHERE group_id=? AND user_id=?", - query_params) - -@pytest.fixture(scope="function") -def fxtr_group_roles(fxtr_group, fxtr_roles):# pylint: disable=[redefined-outer-name,unused-argument] - """Link roles to group""" - group_roles = ( - GroupRole(uuid.UUID("9c25efb2-b477-4918-a95c-9914770cbf4d"), - TEST_GROUP_01, RESOURCE_EDITOR_ROLE), - GroupRole(uuid.UUID("82aed039-fe2f-408c-ab1e-81cd1ba96630"), - TEST_GROUP_02, RESOURCE_READER_ROLE)) - conn, groups = fxtr_group - with db.cursor(conn) as cursor: - cursor.executemany( - "INSERT INTO group_roles VALUES (?, ?, ?)", - ((str(role.group_role_id), str(role.group.group_id), - str(role.role.role_id)) - for role in group_roles)) - - yield conn, groups, group_roles - - with db.cursor(conn) as cursor: - cursor.execute("SELECT * FROM group_user_roles_on_resources") - cursor.executemany( - ("DELETE FROM group_roles " - "WHERE group_role_id=? AND group_id=? AND role_id=?"), - ((str(role.group_role_id), str(role.group.group_id), - str(role.role.role_id)) - for role in group_roles)) - -@pytest.fixture(scope="function") -def fxtr_group_user_roles(fxtr_resources, fxtr_group_roles, fxtr_users_in_group):#pylint: disable=[redefined-outer-name,unused-argument] - """Assign roles to users.""" - conn, _groups, group_roles = fxtr_group_roles - _conn, group_resources = fxtr_resources - _conn, _group, group_users = fxtr_users_in_group - users = tuple(user for user in group_users if user.email - not in ("unaff@iliated.user", "group@lead.er")) - users_roles_resources = ( - (user, RESOURCE_EDITOR_ROLE, TEST_RESOURCES_GROUP_01[1]) - for user in users if user.email == "group@mem.ber01") - with db.cursor(conn) as cursor: - params = tuple({ - "group_id": str(resource.group.group_id), - "user_id": str(user.user_id), - "role_id": str(role.role_id), - "resource_id": str(resource.resource_id) - } for user, role, resource in users_roles_resources) - cursor.executemany( - ("INSERT INTO group_user_roles_on_resources " - "VALUES (:group_id, :user_id, :role_id, :resource_id)"), - params) - - yield conn, group_users, group_roles, group_resources - - with db.cursor(conn) as cursor: - cursor.executemany( - ("DELETE FROM group_user_roles_on_resources WHERE " - "group_id=:group_id AND user_id=:user_id AND role_id=:role_id AND " - "resource_id=:resource_id"), - params) diff --git a/tests/unit/auth/fixtures/migration_fixtures.py b/tests/unit/auth/fixtures/migration_fixtures.py deleted file mode 100644 index eb42c2b..0000000 --- a/tests/unit/auth/fixtures/migration_fixtures.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Fixtures and utilities for migration-related tests""" -import pytest -from yoyo.backends import DatabaseBackend -from yoyo import get_backend, read_migrations -from yoyo.migrations import Migration, MigrationList - -from gn3.auth import db -from gn3.migrations import apply_migrations, rollback_migrations - -@pytest.fixture(scope="session") -def auth_testdb_path(fxtr_app_config): # pylint: disable=redefined-outer-name - """Get the test application's auth database file""" - return fxtr_app_config["AUTH_DB"] - -@pytest.fixture(scope="session") -def auth_migrations_dir(fxtr_app_config): # pylint: disable=redefined-outer-name - """Get the test application's auth database file""" - return fxtr_app_config["AUTH_MIGRATIONS"] - -def apply_single_migration(backend: DatabaseBackend, migration: Migration):# pylint: disable=[redefined-outer-name] - """Utility to apply a single migration""" - apply_migrations(backend, MigrationList([migration])) - -def rollback_single_migration(backend: DatabaseBackend, migration: Migration):# pylint: disable=[redefined-outer-name] - """Utility to rollback a single migration""" - rollback_migrations(backend, MigrationList([migration])) - -@pytest.fixture(scope="session") -def backend(auth_testdb_path):# pylint: disable=redefined-outer-name - """Fixture: retrieve yoyo backend for auth database""" - return get_backend(f"sqlite:///{auth_testdb_path}") - -@pytest.fixture(scope="session") -def all_migrations(auth_migrations_dir): # pylint: disable=redefined-outer-name - """Retrieve all the migrations""" - return read_migrations(auth_migrations_dir) - -@pytest.fixture(scope="function") -def conn_after_auth_migrations(backend, auth_testdb_path, all_migrations): # pylint: disable=redefined-outer-name - """Run all migrations and return a connection to the database after""" - apply_migrations(backend, all_migrations) - with db.connection(auth_testdb_path) as conn: - yield conn - - rollback_migrations(backend, all_migrations) - -def migrations_up_to(migration, migrations_dir): - """Run all the migration before `migration`.""" - migrations = read_migrations(migrations_dir) - index = [mig.path for mig in migrations].index(migration) - return MigrationList(migrations[0:index]) diff --git a/tests/unit/auth/fixtures/oauth2_client_fixtures.py b/tests/unit/auth/fixtures/oauth2_client_fixtures.py deleted file mode 100644 index 654d048..0000000 --- a/tests/unit/auth/fixtures/oauth2_client_fixtures.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Fixtures for OAuth2 clients""" -import uuid -import json -import datetime - -import pytest - -from gn3.auth import db -from gn3.auth.authentication.users import hash_password -from gn3.auth.authentication.oauth2.models.oauth2client import OAuth2Client - -@pytest.fixture(autouse=True) -def fxtr_patch_envvars(monkeypatch): - """Fixture: patch environment variable""" - monkeypatch.setenv("AUTHLIB_INSECURE_TRANSPORT", "true") - -@pytest.fixture -def fxtr_oauth2_clients(fxtr_users_with_passwords): - """Fixture: Create the OAuth2 clients for use with tests.""" - conn, users = fxtr_users_with_passwords - now = datetime.datetime.now() - - clients = tuple( - OAuth2Client(str(uuid.uuid4()), f"yabadabadoo_{idx:03}", now, - now + datetime.timedelta(hours = 2), - { - "client_name": f"test_client_{idx:03}", - "scope": ["profile", "group", "role", "resource", "register-client"], - "redirect_uri": "/test_oauth2", - "token_endpoint_auth_method": [ - "client_secret_post", "client_secret_basic"], - "grant_types": ["password", "authorisation_code", "refresh_token"], - "response_type": "token" - }, user) - for idx, user in enumerate(users, start=1)) - - with db.cursor(conn) as cursor: - cursor.executemany( - "INSERT INTO oauth2_clients VALUES (?, ?, ?, ?, ?, ?)", - ((str(client.client_id), hash_password(client.client_secret), - int(client.client_id_issued_at.timestamp()), - int(client.client_secret_expires_at.timestamp()), - json.dumps(client.client_metadata), str(client.user.user_id)) - for client in clients)) - - yield conn, clients - - with db.cursor(conn) as cursor: - cursor.executemany( - "DELETE FROM oauth2_clients WHERE client_id=?", - ((str(client.client_id),) for client in clients)) diff --git a/tests/unit/auth/fixtures/resource_fixtures.py b/tests/unit/auth/fixtures/resource_fixtures.py deleted file mode 100644 index 117b4f4..0000000 --- a/tests/unit/auth/fixtures/resource_fixtures.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Fixtures and utilities for resource-related tests""" -import pytest - -from gn3.auth import db - -from .group_fixtures import TEST_RESOURCES - -@pytest.fixture(scope="function") -def fxtr_resources(fxtr_group):# pylint: disable=[redefined-outer-name] - """fixture: setup test resources in the database""" - conn, _group = fxtr_group - with db.cursor(conn) as cursor: - cursor.executemany( - "INSERT INTO resources VALUES (?,?,?,?,?)", - ((str(res.group.group_id), str(res.resource_id), res.resource_name, - str(res.resource_category.resource_category_id), - 1 if res.public else 0) for res in TEST_RESOURCES)) - - yield (conn, TEST_RESOURCES) - - with db.cursor(conn) as cursor: - cursor.executemany( - "DELETE FROM resources WHERE group_id=? AND resource_id=?", - ((str(res.group.group_id), str(res.resource_id),) - for res in TEST_RESOURCES)) diff --git a/tests/unit/auth/fixtures/role_fixtures.py b/tests/unit/auth/fixtures/role_fixtures.py deleted file mode 100644 index ee86aa2..0000000 --- a/tests/unit/auth/fixtures/role_fixtures.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Fixtures and utilities for role-related tests""" -import uuid - -import pytest - -from gn3.auth import db -from gn3.auth.authorisation.roles import Role -from gn3.auth.authorisation.privileges import Privilege - -RESOURCE_READER_ROLE = Role( - uuid.UUID("c3ca2507-ee24-4835-9b31-8c21e1c072d3"), "resource_reader", True, - (Privilege("group:resource:view-resource", - "view a resource and use it in computations"),)) - -RESOURCE_EDITOR_ROLE = Role( - uuid.UUID("89819f84-6346-488b-8955-86062e9eedb7"), "resource_editor", True, - ( - Privilege("group:resource:view-resource", - "view a resource and use it in computations"), - Privilege("group:resource:edit-resource", "edit/update a resource"))) - -TEST_ROLES = (RESOURCE_READER_ROLE, RESOURCE_EDITOR_ROLE) - -@pytest.fixture(scope="function") -def fxtr_roles(conn_after_auth_migrations): - """Setup some example roles.""" - with db.cursor(conn_after_auth_migrations) as cursor: - cursor.executemany( - ("INSERT INTO roles VALUES (?, ?, ?)"), - ((str(role.role_id), role.role_name, 1) for role in TEST_ROLES)) - cursor.executemany( - ("INSERT INTO role_privileges VALUES (?, ?)"), - ((str(role.role_id), str(privilege.privilege_id)) - for role in TEST_ROLES for privilege in role.privileges)) - - yield conn_after_auth_migrations, TEST_ROLES - - with db.cursor(conn_after_auth_migrations) as cursor: - cursor.executemany( - ("DELETE FROM role_privileges WHERE role_id=? AND privilege_id=?"), - ((str(role.role_id), str(privilege.privilege_id)) - for role in TEST_ROLES for privilege in role.privileges)) - cursor.executemany( - ("DELETE FROM roles WHERE role_id=?"), - ((str(role.role_id),) for role in TEST_ROLES)) diff --git a/tests/unit/auth/fixtures/user_fixtures.py b/tests/unit/auth/fixtures/user_fixtures.py deleted file mode 100644 index d248f54..0000000 --- a/tests/unit/auth/fixtures/user_fixtures.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Fixtures and utilities for user-related tests""" -import uuid - -import pytest - -from gn3.auth import db -from gn3.auth.authentication.users import User, hash_password - -TEST_USERS = ( - User(uuid.UUID("ecb52977-3004-469e-9428-2a1856725c7f"), "group@lead.er", - "Group Leader"), - User(uuid.UUID("21351b66-8aad-475b-84ac-53ce528451e3"), - "group@mem.ber01", "Group Member 01"), - User(uuid.UUID("ae9c6245-0966-41a5-9a5e-20885a96bea7"), - "group@mem.ber02", "Group Member 02"), - User(uuid.UUID("9a0c7ce5-2f40-4e78-979e-bf3527a59579"), - "unaff@iliated.user", "Unaffiliated User")) - -@pytest.fixture(scope="function") -def fxtr_users(conn_after_auth_migrations):# pylint: disable=[redefined-outer-name] - """Fixture: setup test users.""" - query = "INSERT INTO users(user_id, email, name) VALUES (?, ?, ?)" - query_user_roles = "INSERT INTO user_roles(user_id, role_id) VALUES (?, ?)" - test_user_roles = ( - ("ecb52977-3004-469e-9428-2a1856725c7f", - "a0e67630-d502-4b9f-b23f-6805d0f30e30"), - ("ecb52977-3004-469e-9428-2a1856725c7f", - "ade7e6b0-ba9c-4b51-87d0-2af7fe39a347")) - with db.cursor(conn_after_auth_migrations) as cursor: - cursor.executemany(query, ( - (str(user.user_id), user.email, user.name) for user in TEST_USERS)) - cursor.executemany(query_user_roles, test_user_roles) - - yield (conn_after_auth_migrations, TEST_USERS) - - with db.cursor(conn_after_auth_migrations) as cursor: - cursor.executemany( - "DELETE FROM user_roles WHERE user_id=?", - (("ecb52977-3004-469e-9428-2a1856725c7f",),)) - cursor.executemany( - "DELETE FROM users WHERE user_id=?", - (("ecb52977-3004-469e-9428-2a1856725c7f",), - ("21351b66-8aad-475b-84ac-53ce528451e3",), - ("ae9c6245-0966-41a5-9a5e-20885a96bea7",), - ("9a0c7ce5-2f40-4e78-979e-bf3527a59579",))) - -@pytest.fixture(scope="function") -def fxtr_users_with_passwords(fxtr_users): # pylint: disable=[redefined-outer-name] - """Fixture: add passwords to the users""" - conn, users = fxtr_users - user_passwords_params = tuple( - (str(user.user_id), hash_password( - f"password_for_user_{idx:03}".encode("utf8"))) - for idx, user in enumerate(users, start=1)) - - with db.cursor(conn) as cursor: - cursor.executemany( - "INSERT INTO user_credentials VALUES (?, ?)", - user_passwords_params) - - yield conn, users - - with db.cursor(conn) as cursor: - cursor.executemany( - "DELETE FROM user_credentials WHERE user_id=?", - ((item[0],) for item in user_passwords_params)) diff --git a/tests/unit/auth/test_credentials.py b/tests/unit/auth/test_credentials.py deleted file mode 100644 index f2a3d25..0000000 --- a/tests/unit/auth/test_credentials.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Test the credentials checks""" -import pytest -from yoyo.migrations import MigrationList -from hypothesis import given, settings, strategies, HealthCheck - -from gn3.auth import db -from gn3.auth.authentication import credentials_in_database -from gn3.migrations import get_migration, apply_migrations, rollback_migrations - -from tests.unit.auth.conftest import migrations_up_to - -@pytest.fixture -def with_credentials_table(backend, auth_testdb_path): - """ - Fixture: Yield a connection object with the 'user_credentials' table - created. - """ - migrations_dir = "migrations/auth" - migration = f"{migrations_dir}/20221103_02_sGrIs-create-user-credentials-table.py" - migrations = (migrations_up_to(migration, migrations_dir) + - MigrationList([get_migration(migration)])) - apply_migrations(backend, migrations) - with db.connection(auth_testdb_path) as conn: - yield conn - - rollback_migrations(backend, migrations) - -@pytest.fixture -def with_credentials(with_credentials_table):# pylint: disable=redefined-outer-name - """ - Fixture: Initialise the database with some user credentials. - """ - with db.cursor(with_credentials_table) as cursor: - cursor.executemany( - "INSERT INTO users VALUES (:user_id, :email, :name)", - ({"user_id": "82552014-21ee-4321-b96a-b8788b97b862", - "email": "first@test.user", - "name": "First Test User" - }, - {"user_id": "bdd5cb7a-072d-4c2b-9872-d0cecb718523", - "email": "second@test.user", - "name": "Second Test User" - })) - cursor.executemany( - "INSERT INTO user_credentials VALUES (:user_id, :password)", - ({"user_id": "82552014-21ee-4321-b96a-b8788b97b862", - "password": b'$2b$12$LAh1PYtUgAFK7d5fA0EfL.4AdTZuYEAfzwO.p.jXVboxcP8bXNj7a' - }, - {"user_id": "bdd5cb7a-072d-4c2b-9872-d0cecb718523", - "password": b'$2b$12$zX77QCFSJuwIjAZGc0Jq5.rCWMHEMKD9Zf3Ay4C0AzwsiZ7SSPdKO' - })) - - yield with_credentials_table - - cursor.executemany("DELETE FROM user_credentials WHERE user_id=?", - (("82552014-21ee-4321-b96a-b8788b97b862",), - ("bdd5cb7a-072d-4c2b-9872-d0cecb718523",))) - cursor.executemany("DELETE FROM users WHERE user_id=?", - (("82552014-21ee-4321-b96a-b8788b97b862",), - ("bdd5cb7a-072d-4c2b-9872-d0cecb718523",))) - -@pytest.mark.unit_test -@given(strategies.emails(), strategies.text()) -@settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) -def test_credentials_not_in_database(with_credentials, email, password):# pylint: disable=redefined-outer-name - """ - 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 db.cursor(with_credentials) as cursor: - assert credentials_in_database(cursor, email, password) is False - -@pytest.mark.unit_test -@pytest.mark.parametrize( - "email,password", - (("first@test.user", "wrongpassword"), - ("first@tes.user", "testuser01"))) -def test_partially_wrong_credentials(with_credentials, email, password):# pylint: disable=redefined-outer-name - """ - GIVEN: credentials that exist in the database - WHEN: the credentials are checked with partially wrong values - THEN: the check fails since the credentials are not correct - """ - with db.cursor(with_credentials) as cursor: - assert credentials_in_database(cursor, email, password) is False - -@pytest.mark.unit_test -@pytest.mark.parametrize( - "email,password", - (("first@test.user", "testuser01"), - ("second@test.user", "testuser02"))) -def test_partially_correct_credentials(with_credentials, email, password):# pylint: disable=redefined-outer-name - """ - GIVEN: credentials that exist in the database - WHEN: the credentials are checked with correct values - THEN: the check passes - """ - with db.cursor(with_credentials) as cursor: - assert credentials_in_database(cursor, email, password) is True diff --git a/tests/unit/auth/test_groups.py b/tests/unit/auth/test_groups.py deleted file mode 100644 index d3b8fd4..0000000 --- a/tests/unit/auth/test_groups.py +++ /dev/null @@ -1,171 +0,0 @@ -"""Test functions dealing with group management.""" -from uuid import UUID - -import pytest -from pymonad.maybe import Nothing - -from gn3.auth import db -from gn3.auth.authentication.users import User -from gn3.auth.authorisation.roles import Role -from gn3.auth.authorisation.privileges import Privilege -from gn3.auth.authorisation.errors import AuthorisationError -from gn3.auth.authorisation.groups.models import ( - Group, GroupRole, user_group, create_group, create_group_role) - -from tests.unit.auth import conftest - -create_group_failure = { - "status": "error", - "message": "Unauthorised: Failed to create group." -} - -def uuid_fn(): - """Mock function for uuid""" - return UUID("d32611e3-07fc-4564-b56c-786c6db6de2b") - - -GROUP = Group(UUID("9988c21d-f02f-4d45-8966-22c968ac2fbf"), "TheTestGroup", - {"group_description": "The test group"}) -PRIVILEGES = ( - Privilege( - "group:resource:view-resource", - "view a resource and use it in computations"), - Privilege("group:resource:edit-resource", "edit/update a resource")) - -@pytest.mark.unit_test -@pytest.mark.parametrize( - "user,expected", tuple(zip(conftest.TEST_USERS[0:1], ( - Group( - UUID("d32611e3-07fc-4564-b56c-786c6db6de2b"), "a_test_group", - {"group_description": "A test group"}), - create_group_failure, create_group_failure, create_group_failure, - create_group_failure)))) -def test_create_group(# pylint: disable=[too-many-arguments] - fxtr_app, auth_testdb_path, mocker, fxtr_users, user, expected):# pylint: disable=[unused-argument] - """ - GIVEN: an authenticated user - WHEN: the user attempts to create a group - THEN: verify they are only able to create the group if they have the - appropriate privileges - """ - mocker.patch("gn3.auth.authorisation.groups.models.uuid4", uuid_fn) - mocker.patch("gn3.auth.authorisation.checks.require_oauth.acquire", - conftest.get_tokeniser(user)) - with db.connection(auth_testdb_path) as conn: - assert create_group( - conn, "a_test_group", user, "A test group") == expected - -@pytest.mark.unit_test -@pytest.mark.parametrize("user", conftest.TEST_USERS[1:]) -def test_create_group_raises_exception_with_non_privileged_user(# pylint: disable=[too-many-arguments] - fxtr_app, auth_testdb_path, mocker, fxtr_users, user):# pylint: disable=[unused-argument] - """ - GIVEN: an authenticated user, without appropriate privileges - WHEN: the user attempts to create a group - THEN: verify the system raises an exception - """ - mocker.patch("gn3.auth.authorisation.groups.models.uuid4", uuid_fn) - mocker.patch("gn3.auth.authorisation.checks.require_oauth.acquire", - conftest.get_tokeniser(user)) - with db.connection(auth_testdb_path) as conn: - with pytest.raises(AuthorisationError): - assert create_group(conn, "a_test_group", user, "A test group") - -create_role_failure = { - "status": "error", - "message": "Unauthorised: Could not create the group role" -} - -@pytest.mark.unit_test -@pytest.mark.parametrize( - "user,expected", tuple(zip(conftest.TEST_USERS[0:1], ( - GroupRole( - UUID("d32611e3-07fc-4564-b56c-786c6db6de2b"), - GROUP, - Role(UUID("d32611e3-07fc-4564-b56c-786c6db6de2b"), - "ResourceEditor", True, PRIVILEGES)),)))) -def test_create_group_role(mocker, fxtr_users_in_group, user, expected): - """ - GIVEN: an authenticated user - WHEN: the user attempts to create a role, attached to a group - THEN: verify they are only able to create the role if they have the - appropriate privileges and that the role is attached to the given group - """ - mocker.patch("gn3.auth.authorisation.groups.models.uuid4", uuid_fn) - mocker.patch("gn3.auth.authorisation.roles.models.uuid4", uuid_fn) - mocker.patch("gn3.auth.authorisation.checks.require_oauth.acquire", - conftest.get_tokeniser(user)) - conn, _group, _users = fxtr_users_in_group - with db.cursor(conn) as cursor: - assert create_group_role( - conn, GROUP, "ResourceEditor", PRIVILEGES) == expected - # cleanup - cursor.execute( - ("DELETE FROM group_roles " - "WHERE group_role_id=? AND group_id=? AND role_id=?"), - (str(uuid_fn()), str(GROUP.group_id), str(uuid_fn()))) - -@pytest.mark.unit_test -@pytest.mark.parametrize( - "user,expected", tuple(zip(conftest.TEST_USERS[1:], ( - create_role_failure, create_role_failure, create_role_failure)))) -def test_create_group_role_raises_exception_with_unauthorised_users( - mocker, fxtr_users_in_group, user, expected): - """ - GIVEN: an authenticated user - WHEN: the user attempts to create a role, attached to a group - THEN: verify they are only able to create the role if they have the - appropriate privileges and that the role is attached to the given group - """ - mocker.patch("gn3.auth.authorisation.groups.models.uuid4", uuid_fn) - mocker.patch("gn3.auth.authorisation.roles.models.uuid4", uuid_fn) - mocker.patch("gn3.auth.authorisation.checks.require_oauth.acquire", - conftest.get_tokeniser(user)) - conn, _group, _users = fxtr_users_in_group - with pytest.raises(AuthorisationError): - assert create_group_role( - conn, GROUP, "ResourceEditor", PRIVILEGES) == expected - -@pytest.mark.unit_test -def test_create_multiple_groups(mocker, fxtr_users): - """ - GIVEN: An authenticated user with appropriate authorisation - WHEN: The user attempts to create a new group, while being a member of an - existing group - THEN: The system should prevent that, and respond with an appropriate error - message - """ - mocker.patch("gn3.auth.authorisation.groups.models.uuid4", uuid_fn) - user = User( - UUID("ecb52977-3004-469e-9428-2a1856725c7f"), "group@lead.er", - "Group Leader") - mocker.patch("gn3.auth.authorisation.checks.require_oauth.acquire", - conftest.get_tokeniser(user)) - conn, _test_users = fxtr_users - # First time, successfully creates the group - assert create_group(conn, "a_test_group", user) == Group( - UUID("d32611e3-07fc-4564-b56c-786c6db6de2b"), "a_test_group", - {}) - # subsequent attempts should fail - with pytest.raises(AuthorisationError): - create_group(conn, "another_test_group", user) - -@pytest.mark.unit_test -@pytest.mark.parametrize( - "user,expected", - tuple(zip( - conftest.TEST_USERS, - (([Group(UUID("9988c21d-f02f-4d45-8966-22c968ac2fbf"), "TheTestGroup", {})] * 3) - + [Nothing])))) -def test_user_group(fxtr_users_in_group, user, expected): - """ - GIVEN: A bunch of registered users, some of whom are members of a group, and - others are not - WHEN: a particular user's group is requested, - THEN: return a Maybe containing the group that the user belongs to, or - Nothing - """ - conn, _group, _users = fxtr_users_in_group - assert ( - user_group(conn, user).maybe(Nothing, lambda val: val) - == expected) diff --git a/tests/unit/auth/test_migrations_add_data_to_table.py b/tests/unit/auth/test_migrations_add_data_to_table.py deleted file mode 100644 index 9cb5d0c..0000000 --- a/tests/unit/auth/test_migrations_add_data_to_table.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Test data insertion when migrations are run.""" -import pytest - -from gn3.auth import db -from gn3.migrations import get_migration, apply_migrations, rollback_migrations -from tests.unit.auth.conftest import ( - apply_single_migration, rollback_single_migration, migrations_up_to) - -test_params = ( - ("20221116_01_nKUmX-add-privileges-to-group-leader-role.py", - ("SELECT role_id, privilege_id FROM role_privileges " - "WHERE role_id=? AND privilege_id IN (?, ?, ?, ?)"), - ("a0e67630-d502-4b9f-b23f-6805d0f30e30", - "221660b1-df05-4be1-b639-f010269dbda9", - "7bcca363-cba9-4169-9e31-26bdc6179b28", - "5103cc68-96f8-4ebb-83a4-a31692402c9b", - "1c59eff5-9336-4ed2-a166-8f70d4cb012e"), - (("a0e67630-d502-4b9f-b23f-6805d0f30e30", - "221660b1-df05-4be1-b639-f010269dbda9"), - ("a0e67630-d502-4b9f-b23f-6805d0f30e30", - "7bcca363-cba9-4169-9e31-26bdc6179b28"), - ("a0e67630-d502-4b9f-b23f-6805d0f30e30", - "5103cc68-96f8-4ebb-83a4-a31692402c9b"), - ("a0e67630-d502-4b9f-b23f-6805d0f30e30", - "1c59eff5-9336-4ed2-a166-8f70d4cb012e"))),) - -@pytest.mark.unit_test -@pytest.mark.parametrize("migration_file,query,query_params,data", test_params) -def test_apply_insert(# pylint: disable=[too-many-arguments] - auth_migrations_dir, backend, auth_testdb_path, migration_file, query, - query_params, data): - """ - GIVEN: a database migration script - WHEN: the script is applied - THEN: ensure the given data exists in the table - """ - migration_path=f"{auth_migrations_dir}/{migration_file}" - older_migrations = migrations_up_to(migration_path, auth_migrations_dir) - the_migration = get_migration(migration_path) - apply_migrations(backend, older_migrations) - with db.connection(auth_testdb_path, None) as conn, db.cursor(conn) as cursor: - cursor.execute(query, query_params) - result_before_migration = cursor.fetchall() - apply_single_migration(backend, the_migration) - cursor.execute(query, query_params) - result_after_migration = cursor.fetchall() - - rollback_migrations(backend, older_migrations + [the_migration]) - assert len(result_before_migration) == 0, "Expected no results before migration" - assert sorted(result_after_migration) == sorted(data) - -@pytest.mark.unit_test -@pytest.mark.parametrize("migration_file,query,query_params,data", test_params) -def test_rollback_insert(# pylint: disable=[too-many-arguments] - auth_migrations_dir, backend, auth_testdb_path, migration_file, query, - query_params, data): - """ - GIVEN: a database migration script - WHEN: the script is rolled back - THEN: ensure the given data no longer exists in the database - """ - migration_path=f"{auth_migrations_dir}/{migration_file}" - older_migrations = migrations_up_to(migration_path, auth_migrations_dir) - the_migration = get_migration(migration_path) - apply_migrations(backend, older_migrations) - with db.connection(auth_testdb_path, None) as conn, db.cursor(conn) as cursor: - cursor.execute(query, query_params) - result_before_migration = cursor.fetchall() - apply_single_migration(backend, the_migration) - cursor.execute(query, query_params) - result_after_migration = cursor.fetchall() - rollback_single_migration(backend, the_migration) - cursor.execute(query, query_params) - result_after_rollback = cursor.fetchall() - - rollback_migrations(backend, older_migrations) - assert len(result_before_migration) == 0, "Expected no results before migration" - assert sorted(result_after_migration) == sorted(data) - assert len(result_after_rollback) == 0, "Expected no results after rollback" diff --git a/tests/unit/auth/test_migrations_add_remove_columns.py b/tests/unit/auth/test_migrations_add_remove_columns.py deleted file mode 100644 index ea9bf7b..0000000 --- a/tests/unit/auth/test_migrations_add_remove_columns.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Test migrations that alter tables adding/removing columns.""" -import pytest - -from gn3.auth import db -from gn3.migrations import get_migration, apply_migrations, rollback_migrations -from tests.unit.auth.conftest import ( - apply_single_migration, rollback_single_migration, migrations_up_to) - -QUERY = "SELECT sql FROM sqlite_schema WHERE name=?" - -TEST_PARAMS = ( - ("20221109_01_HbD5F-add-resource-meta-field-to-resource-categories-field.py", - "resource_categories", "resource_meta TEXT", True), - (("20221110_08_23psB-add-privilege-category-and-privilege-description-" - "columns-to-privileges-table.py"), - "privileges", "privilege_category TEXT", True), - (("20221110_08_23psB-add-privilege-category-and-privilege-description-" - "columns-to-privileges-table.py"), - "privileges", "privilege_description TEXT", True), - ("20221117_01_RDlfx-modify-group-roles-add-group-role-id.py", "group_roles", - "group_role_id", True), - ("20221208_01_sSdHz-add-public-column-to-resources-table.py", "resources", - "public", True)) - -def found(haystack: str, needle: str) -> bool: - """Check whether `needle` is found in `haystack`""" - return any( - (line.strip().find(needle) >= 0) for line in haystack.split("\n")) - -def pristine_before_migration(adding: bool, result_str: str, column: str) -> bool: - """Check that database is pristine before running the migration""" - col_was_found = found(result_str, column) - if adding: - return not col_was_found - return col_was_found - -def applied_successfully(adding: bool, result_str: str, column: str) -> bool: - """Check that the migration ran successfully""" - col_was_found = found(result_str, column) - if adding: - return col_was_found - return not col_was_found - -def rolled_back_successfully(adding: bool, result_str: str, column: str) -> bool: - """Check that the migration ran successfully""" - col_was_found = found(result_str, column) - if adding: - return not col_was_found - return col_was_found - -@pytest.mark.unit_test -@pytest.mark.parametrize( - "migration_file,the_table,the_column,adding", TEST_PARAMS) -def test_apply_add_remove_column(# pylint: disable=[too-many-arguments] - auth_migrations_dir, auth_testdb_path, backend, migration_file, - the_table, the_column, adding): - """ - GIVEN: A migration that alters a table, adding or removing a column - WHEN: The migration is applied - THEN: Ensure the column exists if `adding` is True, otherwise, ensure the - column has been dropped - """ - migration_path = f"{auth_migrations_dir}/{migration_file}" - older_migrations = migrations_up_to(migration_path, auth_migrations_dir) - the_migration = get_migration(migration_path) - apply_migrations(backend, older_migrations) - with db.connection(auth_testdb_path) as conn, db.cursor(conn) as cursor: - cursor.execute(QUERY, (the_table,)) - results_before_migration = cursor.fetchone() - apply_single_migration(backend, the_migration) - cursor.execute(QUERY, (the_table,)) - results_after_migration = cursor.fetchone() - - rollback_migrations(backend, older_migrations + [the_migration]) - - assert pristine_before_migration( - adding, results_before_migration[0], the_column), ( - f"Column `{the_column}` exists before migration and should not" - if adding else - f"Column `{the_column}` doesn't exist before migration and it should") - assert applied_successfully( - adding, results_after_migration[0], the_column), "Migration failed" - -@pytest.mark.unit_test -@pytest.mark.parametrize( - "migration_file,the_table,the_column,adding", TEST_PARAMS) -def test_rollback_add_remove_column(# pylint: disable=[too-many-arguments] - auth_migrations_dir, auth_testdb_path, backend, migration_file, - the_table, the_column, adding): - """ - GIVEN: A migration that alters a table, adding or removing a column - WHEN: The migration is applied - THEN: Ensure the column is dropped if `adding` is True, otherwise, ensure - the column has been restored - """ - migration_path = f"{auth_migrations_dir}/{migration_file}" - older_migrations = migrations_up_to(migration_path, auth_migrations_dir) - the_migration = get_migration(migration_path) - apply_migrations(backend, older_migrations) - apply_single_migration(backend, the_migration) - with db.connection(auth_testdb_path) as conn, db.cursor(conn) as cursor: - cursor.execute(QUERY, (the_table,)) - results_before_rollback = cursor.fetchone() - rollback_single_migration(backend, the_migration) - cursor.execute(QUERY, (the_table,)) - results_after_rollback = cursor.fetchone() - - rollback_migrations(backend, older_migrations + [the_migration]) - - assert pristine_before_migration( - not adding, results_before_rollback[0], the_column), ( - f"Column `{the_column}` doesn't exist before rollback and it should" - if adding else - f"Column `{the_column}` exists before rollback and should not") - assert rolled_back_successfully( - adding, results_after_rollback[0], the_column), "Rollback failed" diff --git a/tests/unit/auth/test_migrations_create_tables.py b/tests/unit/auth/test_migrations_create_tables.py deleted file mode 100644 index 2b8140b..0000000 --- a/tests/unit/auth/test_migrations_create_tables.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Test migrations that create tables""" -import pytest - -from gn3.auth import db -from gn3.migrations import get_migration, apply_migrations, rollback_migrations -from tests.unit.auth.conftest import ( - apply_single_migration, rollback_single_migration, migrations_up_to) - -migrations_and_tables = ( - ("20221103_01_js9ub-initialise-the-auth-entic-oris-ation-database.py", - "users"), - ("20221103_02_sGrIs-create-user-credentials-table.py", "user_credentials"), - ("20221108_01_CoxYh-create-the-groups-table.py", "groups"), - ("20221108_02_wxTr9-create-privileges-table.py", "privileges"), - ("20221108_03_Pbhb1-create-resource-categories-table.py", "resource_categories"), - ("20221110_01_WtZ1I-create-resources-table.py", "resources"), - ("20221110_05_BaNtL-create-roles-table.py", "roles"), - ("20221110_06_Pq2kT-create-generic-roles-table.py", "generic_roles"), - ("20221110_07_7WGa1-create-role-privileges-table.py", "role_privileges"), - ("20221114_01_n8gsF-create-generic-role-privileges-table.py", - "generic_role_privileges"), - ("20221114_03_PtWjc-create-group-roles-table.py", "group_roles"), - ("20221114_05_hQun6-create-user-roles-table.py", "user_roles"), - ("20221117_02_fmuZh-create-group-users-table.py", "group_users"), - ("20221206_01_BbeF9-create-group-user-roles-on-resources-table.py", - "group_user_roles_on_resources"), - ("20221219_01_CI3tN-create-oauth2-clients-table.py", "oauth2_clients"), - ("20221219_02_buSEU-create-oauth2-tokens-table.py", "oauth2_tokens"), - ("20221219_03_PcTrb-create-authorisation-code-table.py", - "authorisation_code"), - ("20230207_01_r0bkZ-create-group-join-requests-table.py", - "group_join_requests"), - ("20230322_01_0dDZR-create-linked-phenotype-data-table.py", - "linked_phenotype_data"), - ("20230322_02_Ll854-create-phenotype-resources-table.py", - "phenotype_resources"), - ("20230404_01_VKxXg-create-linked-genotype-data-table.py", - "linked_genotype_data"), - ("20230404_02_la33P-create-genotype-resources-table.py", - "genotype_resources"), - ("20230410_01_8mwaf-create-linked-mrna-data-table.py", "linked_mrna_data"), - ("20230410_02_WZqSf-create-mrna-resources-table.py", "mrna_resources")) - -@pytest.mark.unit_test -@pytest.mark.parametrize("migration_file,the_table", migrations_and_tables) -def test_create_table( - auth_testdb_path, auth_migrations_dir, backend, migration_file, - the_table): - """ - GIVEN: A database migration script to create table, `the_table` - WHEN: The migration is applied - THEN: Ensure that the table `the_table` is created - """ - migration_path=f"{auth_migrations_dir}/{migration_file}" - older_migrations = migrations_up_to(migration_path, auth_migrations_dir) - apply_migrations(backend, older_migrations) - with db.connection(auth_testdb_path) as conn, db.cursor(conn) as cursor: - cursor.execute("SELECT name FROM sqlite_schema WHERE type='table'") - result_before_migration = cursor.fetchall() - apply_single_migration(backend, get_migration(migration_path)) - cursor.execute("SELECT name FROM sqlite_schema WHERE type='table'") - result_after_migration = cursor.fetchall() - - rollback_migrations(backend, older_migrations) - assert the_table not in [row[0] for row in result_before_migration] - assert the_table in [row[0] for row in result_after_migration] - -@pytest.mark.unit_test -@pytest.mark.parametrize("migration_file,the_table", migrations_and_tables) -def test_rollback_create_table( - auth_testdb_path, auth_migrations_dir, backend, migration_file, - the_table): - """ - GIVEN: A database migration script to create the table `the_table` - WHEN: The migration is rolled back - THEN: Ensure that the table `the_table` no longer exists - """ - migration_path=f"{auth_migrations_dir}/{migration_file}" - older_migrations = migrations_up_to(migration_path, auth_migrations_dir) - apply_migrations(backend, older_migrations) - with db.connection(auth_testdb_path) as conn, db.cursor(conn) as cursor: - apply_single_migration(backend, get_migration(migration_path)) - cursor.execute("SELECT name FROM sqlite_schema WHERE type='table'") - result_after_migration = cursor.fetchall() - rollback_single_migration(backend, get_migration(migration_path)) - cursor.execute("SELECT name FROM sqlite_schema WHERE type='table'") - result_after_rollback = cursor.fetchall() - - rollback_migrations(backend, older_migrations) - assert the_table in [row[0] for row in result_after_migration] - assert the_table not in [row[0] for row in result_after_rollback] diff --git a/tests/unit/auth/test_migrations_drop_tables.py b/tests/unit/auth/test_migrations_drop_tables.py deleted file mode 100644 index 2362c77..0000000 --- a/tests/unit/auth/test_migrations_drop_tables.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Test migrations that create tables""" - -import pytest - -from gn3.auth import db -from gn3.migrations import get_migration, apply_migrations, rollback_migrations -from tests.unit.auth.conftest import ( - apply_single_migration, rollback_single_migration, migrations_up_to) - -test_params = ( - ("20221114_02_DKKjn-drop-generic-role-tables.py", "generic_roles"), - ("20221114_02_DKKjn-drop-generic-role-tables.py", "generic_role_privileges")) - -@pytest.mark.unit_test -@pytest.mark.parametrize("migration_file,the_table", test_params) -def test_drop_table( - auth_testdb_path, auth_migrations_dir, backend, - migration_file, the_table): - """ - GIVEN: A database migration script to create table, `the_table` - WHEN: The migration is applied - THEN: Ensure that the table `the_table` is created - """ - migration_path=f"{auth_migrations_dir}/{migration_file}" - older_migrations = migrations_up_to(migration_path, auth_migrations_dir) - the_migration = get_migration(migration_path) - apply_migrations(backend, older_migrations) - with db.connection(auth_testdb_path) as conn, db.cursor(conn) as cursor: - cursor.execute("SELECT name FROM sqlite_schema WHERE type='table'") - result_before_migration = cursor.fetchall() - apply_single_migration(backend, the_migration) - cursor.execute("SELECT name FROM sqlite_schema WHERE type='table'") - result_after_migration = cursor.fetchall() - - rollback_migrations(backend, older_migrations + [the_migration]) - assert the_table in [row[0] for row in result_before_migration] - assert the_table not in [row[0] for row in result_after_migration] - -@pytest.mark.unit_test -@pytest.mark.parametrize("migration_file,the_table", test_params) -def test_rollback_drop_table( - auth_testdb_path, auth_migrations_dir, backend, migration_file, - the_table): - """ - GIVEN: A database migration script to create the table `the_table` - WHEN: The migration is rolled back - THEN: Ensure that the table `the_table` no longer exists - """ - migration_path=f"{auth_migrations_dir}/{migration_file}" - older_migrations = migrations_up_to(migration_path, auth_migrations_dir) - the_migration = get_migration(migration_path) - apply_migrations(backend, older_migrations) - with db.connection(auth_testdb_path) as conn, db.cursor(conn) as cursor: - apply_single_migration(backend, the_migration) - cursor.execute("SELECT name FROM sqlite_schema WHERE type='table'") - result_after_migration = cursor.fetchall() - rollback_single_migration(backend, the_migration) - cursor.execute("SELECT name FROM sqlite_schema WHERE type='table'") - result_after_rollback = cursor.fetchall() - - rollback_migrations(backend, older_migrations) - assert the_table not in [row[0] for row in result_after_migration] - assert the_table in [row[0] for row in result_after_rollback] diff --git a/tests/unit/auth/test_migrations_indexes.py b/tests/unit/auth/test_migrations_indexes.py deleted file mode 100644 index b1f06d9..0000000 --- a/tests/unit/auth/test_migrations_indexes.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Test that indexes are created and removed.""" -import pytest - -from gn3.auth import db -from gn3.migrations import get_migration, apply_migrations, rollback_migrations -from tests.unit.auth.conftest import ( - apply_single_migration, rollback_single_migration, migrations_up_to) - -QUERY = """ -SELECT name FROM sqlite_master WHERE type='index' AND tbl_name = ? -AND name= ? -""" - -migrations_tables_and_indexes = ( - ("20221110_07_7WGa1-create-role-privileges-table.py", "role_privileges", - "idx_tbl_role_privileges_cols_role_id"), - ("20221114_01_n8gsF-create-generic-role-privileges-table.py", - "generic_role_privileges", - "idx_tbl_generic_role_privileges_cols_generic_role_id"), - ("20221114_03_PtWjc-create-group-roles-table.py", "group_roles", - "idx_tbl_group_roles_cols_group_id"), - ("20221114_05_hQun6-create-user-roles-table.py", "user_roles", - "idx_tbl_user_roles_cols_user_id"), - ("20221117_02_fmuZh-create-group-users-table.py", "group_users", - "tbl_group_users_cols_group_id"), - ("20221206_01_BbeF9-create-group-user-roles-on-resources-table.py", - "group_user_roles_on_resources", - "idx_tbl_group_user_roles_on_resources_group_user_resource")) - -@pytest.mark.unit_test -@pytest.mark.parametrize( - "migration_file,the_table,the_index", migrations_tables_and_indexes) -def test_index_created(# pylint: disable=[too-many-arguments] - auth_testdb_path, auth_migrations_dir, backend, migration_file, - the_table, the_index): - """ - GIVEN: A database migration - WHEN: The migration is applied - THEN: Ensure the given index is created for the provided table - """ - migration_path=f"{auth_migrations_dir}/{migration_file}" - older_migrations = migrations_up_to(migration_path, auth_migrations_dir) - the_migration = get_migration(migration_path) - query_params = (the_table, the_index) - apply_migrations(backend, older_migrations) - with db.connection(auth_testdb_path) as conn, db.cursor(conn) as cursor: - cursor.execute(QUERY, query_params) - result_before_migration = cursor.fetchall() - apply_single_migration(backend, the_migration) - cursor.execute(QUERY, query_params) - result_after_migration = cursor.fetchall() - - rollback_migrations(backend, older_migrations + [the_migration]) - assert the_index not in [row[0] for row in result_before_migration], ( - f"Index '{the_index}' was found for table '{the_table}' before migration.") - assert ( - len(result_after_migration) == 1 - and result_after_migration[0][0] == the_index), ( - f"Index '{the_index}' was not found for table '{the_table}' after migration.") - -@pytest.mark.unit_test -@pytest.mark.parametrize( - "migration_file,the_table,the_index", migrations_tables_and_indexes) -def test_index_dropped(# pylint: disable=[too-many-arguments] - auth_testdb_path, auth_migrations_dir, backend, migration_file, - the_table, the_index): - """ - GIVEN: A database migration - WHEN: The migration is rolled-back - THEN: Ensure the given index no longer exists for the given table - """ - migration_path=f"{auth_migrations_dir}/{migration_file}" - older_migrations = migrations_up_to(migration_path, auth_migrations_dir) - the_migration = get_migration(migration_path) - query_params = (the_table, the_index) - apply_migrations(backend, older_migrations) - with db.connection(auth_testdb_path) as conn, db.cursor(conn) as cursor: - cursor.execute(QUERY, query_params) - result_before_migration = cursor.fetchall() - apply_single_migration(backend, the_migration) - cursor.execute(QUERY, query_params) - result_after_migration = cursor.fetchall() - rollback_single_migration(backend, the_migration) - cursor.execute(QUERY, query_params) - result_after_rollback = cursor.fetchall() - - rollback_migrations(backend, older_migrations) - assert the_index not in [row[0] for row in result_before_migration], ( - f"Index '{the_index}' was found for table '{the_table}' before " - "migration") - assert ( - len(result_after_migration) == 1 - and result_after_migration[0][0] == the_index), ( - f"Index '{the_index}' was not found for table '{the_table}' after migration.") - assert the_index not in [row[0] for row in result_after_rollback], ( - f"Index '{the_index}' was found for table '{the_table}' after " - "rollback") diff --git a/tests/unit/auth/test_migrations_init_data_in_resource_categories_table.py b/tests/unit/auth/test_migrations_init_data_in_resource_categories_table.py deleted file mode 100644 index dd3d4c6..0000000 --- a/tests/unit/auth/test_migrations_init_data_in_resource_categories_table.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Test that the `resource_categories` table is initialised with the startup data. -""" -import pytest - -from gn3.auth import db -from gn3.migrations import get_migration, apply_migrations, rollback_migrations -from tests.unit.auth.conftest import ( - apply_single_migration, rollback_single_migration, migrations_up_to) - -MIGRATION_PATH = "migrations/auth/20221108_04_CKcSL-init-data-in-resource-categories-table.py" - -@pytest.mark.unit_test -def test_apply_init_data(auth_testdb_path, auth_migrations_dir, backend): - """ - GIVEN: A migration script - WHEN: The migration is applied - THEN: Verify that the expected data exists in the table - """ - older_migrations = migrations_up_to(MIGRATION_PATH, auth_migrations_dir) - the_migration = get_migration(MIGRATION_PATH) - apply_migrations(backend, older_migrations) - with db.connection(auth_testdb_path, None) as conn, db.cursor(conn) as cursor: - cursor.execute("SELECT * FROM resource_categories") - assert len(cursor.fetchall()) == 0, "Expected empty table." - apply_single_migration(backend, the_migration) - cursor.execute("SELECT * FROM resource_categories") - results = cursor.fetchall() - assert len(results) == 3, "Expected 3 rows of data." - assert sorted(results) == sorted(( - ('fad071a3-2fc8-40b8-992b-cdefe7dcac79', 'mrna', 'mRNA Dataset'), - ('548d684b-d4d1-46fb-a6d3-51a56b7da1b3', 'phenotype', - 'Phenotype (Publish) Dataset'), - ('48056f84-a2a6-41ac-8319-0e1e212cba2a', 'genotype', - 'Genotype Dataset'))) - - rollback_migrations(backend, older_migrations + [the_migration]) - -@pytest.mark.unit_test -def test_rollback_init_data(auth_testdb_path, auth_migrations_dir, backend): - """ - GIVEN: A migration script - WHEN: The migration is rolled back - THEN: Verify that the table is empty - """ - older_migrations = migrations_up_to(MIGRATION_PATH, auth_migrations_dir) - the_migration = get_migration(MIGRATION_PATH) - apply_migrations(backend, older_migrations) - with db.connection(auth_testdb_path, None) as conn, db.cursor(conn) as cursor: - cursor.execute("SELECT * FROM resource_categories") - assert len(cursor.fetchall()) == 0, "Expected empty table." - apply_single_migration(backend, the_migration) - cursor.execute("SELECT * FROM resource_categories") - results = cursor.fetchall() - assert len(results) == 3, "Expected 3 rows of data." - rollback_single_migration(backend, the_migration) - cursor.execute("SELECT * FROM resource_categories") - assert len(cursor.fetchall()) == 0, "Expected empty table." - - rollback_migrations(backend, older_migrations) diff --git a/tests/unit/auth/test_migrations_insert_data_into_empty_table.py b/tests/unit/auth/test_migrations_insert_data_into_empty_table.py deleted file mode 100644 index ebb7fa6..0000000 --- a/tests/unit/auth/test_migrations_insert_data_into_empty_table.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Test data insertion when migrations are run.""" -import sqlite3 -from contextlib import closing - -import pytest - -from gn3.migrations import get_migration, apply_migrations, rollback_migrations -from tests.unit.auth.conftest import ( - apply_single_migration, rollback_single_migration, migrations_up_to) - -test_params = ( - ("20221113_01_7M0hv-enumerate-initial-privileges.py", "privileges", 19), - ("20221114_04_tLUzB-initialise-basic-roles.py", "roles", 2), - ("20221114_04_tLUzB-initialise-basic-roles.py", "role_privileges", 15)) - -@pytest.mark.unit_test -@pytest.mark.parametrize( - "migration_file,table,row_count", test_params) -def test_apply_insert(# pylint: disable=[too-many-arguments] - auth_testdb_path, auth_migrations_dir, backend, migration_file, - table, row_count): - """ - GIVEN: A database migration - WHEN: The migration is applied - THEN: Ensure the given number of rows are inserted into the table - """ - migration_path=f"{auth_migrations_dir}/{migration_file}" - older_migrations = migrations_up_to(migration_path, auth_migrations_dir) - the_migration = get_migration(migration_path) - apply_migrations(backend, older_migrations) - with closing(sqlite3.connect(auth_testdb_path)) as conn, closing(conn.cursor()) as cursor: - query = f"SELECT COUNT(*) FROM {table}" - cursor.execute(query) - result_before_migration = cursor.fetchall() - apply_single_migration(backend, the_migration) - cursor.execute(query) - result_after_migration = cursor.fetchall() - - rollback_migrations(backend, older_migrations+[the_migration]) - assert result_before_migration[0][0] == 0, ( - "Expected empty table before initialisation") - assert result_after_migration[0][0] == row_count, ( - f"Expected {row_count} rows") - -@pytest.mark.unit_test -@pytest.mark.parametrize( - "migration_file,table,row_count", test_params) -def test_rollback_insert(# pylint: disable=[too-many-arguments] - auth_testdb_path, auth_migrations_dir, backend, migration_file, - table, row_count): - """ - GIVEN: A database migration - WHEN: The migration is applied - THEN: Ensure the given number of rows are inserted into the table - """ - migration_path=f"{auth_migrations_dir}/{migration_file}" - older_migrations = migrations_up_to(migration_path, auth_migrations_dir) - the_migration = get_migration(migration_path) - apply_migrations(backend, older_migrations) - with closing(sqlite3.connect(auth_testdb_path)) as conn, closing(conn.cursor()) as cursor: - query = f"SELECT COUNT(*) FROM {table}" - cursor.execute(query) - result_before_migration = cursor.fetchall() - apply_single_migration(backend, the_migration) - cursor.execute(query) - result_after_migration = cursor.fetchall() - rollback_single_migration(backend, the_migration) - cursor.execute(query) - result_after_rollback = cursor.fetchall() - - rollback_migrations(backend, older_migrations) - assert result_before_migration[0][0] == 0, ( - "Expected empty table before initialisation") - assert result_after_migration[0][0] == row_count, ( - f"Expected {row_count} rows") - assert result_after_rollback[0][0] == 0, ( - "Expected empty table after rollback") diff --git a/tests/unit/auth/test_privileges.py b/tests/unit/auth/test_privileges.py deleted file mode 100644 index 3c645c7..0000000 --- a/tests/unit/auth/test_privileges.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Test the privileges module""" -import pytest - -from gn3.auth import db -from gn3.auth.authorisation.privileges import Privilege, user_privileges - -from tests.unit.auth import conftest - - -PRIVILEGES = sorted( - (Privilege("system:group:create-group", "Create a group"), - Privilege("system:group:view-group", "View the details of a group"), - Privilege("system:group:edit-group", "Edit the details of a group"), - Privilege("system:user:list", "List users in the system"), - Privilege("system:group:delete-group", "Delete a group"), - Privilege("group:user:add-group-member", "Add a user to a group"), - Privilege("group:user:remove-group-member", "Remove a user from a group"), - Privilege("system:group:transfer-group-leader", - "Transfer leadership of the group to some other member"), - - Privilege("group:resource:create-resource", "Create a resource object"), - Privilege("group:resource:view-resource", - "view a resource and use it in computations"), - Privilege("group:resource:edit-resource", "edit/update a resource"), - Privilege("group:resource:delete-resource", "Delete a resource"), - - Privilege("group:role:create-role", "Create a new role"), - Privilege("group:role:edit-role", "edit/update an existing role"), - Privilege("group:user:assign-role", "Assign a role to an existing user"), - Privilege("group:role:delete-role", "Delete an existing role")), - key=lambda x: x.privilege_id) - -@pytest.mark.unit_test -@pytest.mark.parametrize( - "user,expected", tuple(zip( - conftest.TEST_USERS, (PRIVILEGES, [], [], [], [])))) -def test_user_privileges(auth_testdb_path, fxtr_users, user, expected):# pylint: disable=[unused-argument] - """ - GIVEN: A user - WHEN: An attempt is made to fetch the user's privileges - THEN: Ensure only - """ - with db.connection(auth_testdb_path) as conn: - assert sorted( - user_privileges(conn, user), key=lambda x: x.privilege_id) == expected diff --git a/tests/unit/auth/test_resources.py b/tests/unit/auth/test_resources.py deleted file mode 100644 index 7b9798a..0000000 --- a/tests/unit/auth/test_resources.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Test resource-management functions""" -import uuid - -import pytest - -from gn3.auth import db - -from gn3.auth.authorisation.groups import Group -from gn3.auth.authorisation.errors import AuthorisationError -from gn3.auth.authorisation.resources.models import ( - Resource, user_resources, create_resource, ResourceCategory, - public_resources) - -from tests.unit.auth import conftest - -group = Group(uuid.UUID("9988c21d-f02f-4d45-8966-22c968ac2fbf"), "TheTestGroup", - {}) -resource_category = ResourceCategory( - uuid.UUID("fad071a3-2fc8-40b8-992b-cdefe7dcac79"), "mrna", "mRNA Dataset") -create_resource_failure = { - "status": "error", - "message": "Unauthorised: Could not create resource" -} - - -def uuid_fn(): - """Mock function for uuid""" - return uuid.UUID("d32611e3-07fc-4564-b56c-786c6db6de2b") - - -@pytest.mark.unit_test -@pytest.mark.parametrize( - "user,expected", - tuple(zip( - conftest.TEST_USERS[0:1], - (Resource( - group, uuid.UUID("d32611e3-07fc-4564-b56c-786c6db6de2b"), - "test_resource", resource_category, False),)))) -def test_create_resource(mocker, fxtr_users_in_group, user, expected): - """Test that resource creation works as expected.""" - mocker.patch("gn3.auth.authorisation.resources.models.uuid4", uuid_fn) - mocker.patch("gn3.auth.authorisation.checks.require_oauth.acquire", - conftest.get_tokeniser(user)) - conn, _group, _users = fxtr_users_in_group - resource = create_resource( - conn, "test_resource", resource_category, user, False) - assert resource == expected - - with db.cursor(conn) as cursor: - # Cleanup - cursor.execute( - "DELETE FROM group_user_roles_on_resources WHERE resource_id=?", - (str(resource.resource_id),)) - cursor.execute( - "DELETE FROM group_roles WHERE group_id=?", - (str(resource.group.group_id),)) - cursor.execute( - "DELETE FROM resources WHERE resource_id=?", - (str(resource.resource_id),)) - -@pytest.mark.unit_test -@pytest.mark.parametrize( - "user,expected", - tuple(zip( - conftest.TEST_USERS[1:], - (create_resource_failure, create_resource_failure, - create_resource_failure)))) -def test_create_resource_raises_for_unauthorised_users( - mocker, fxtr_users_in_group, user, expected): - """Test that resource creation works as expected.""" - mocker.patch("gn3.auth.authorisation.resources.models.uuid4", uuid_fn) - mocker.patch("gn3.auth.authorisation.checks.require_oauth.acquire", - conftest.get_tokeniser(user)) - conn, _group, _users = fxtr_users_in_group - with pytest.raises(AuthorisationError): - assert create_resource( - conn, "test_resource", resource_category, user, False) == expected - - -@pytest.mark.unit_test -def test_public_resources(fxtr_resources): - """ - GIVEN: some resources in the database - WHEN: public resources are requested - THEN: only list the resources that are public - """ - conn, _res = fxtr_resources - assert sorted( - public_resources(conn), - key=lambda resource: resource.resource_id) == sorted(tuple( - res for res in - conftest.TEST_RESOURCES - if res.public), key=lambda resource: resource.resource_id) - -PUBLIC_RESOURCES = sorted( - {res.resource_id: res for res in conftest.TEST_RESOURCES_PUBLIC}.values(), - key=lambda resource: resource.resource_id) - -@pytest.mark.unit_test -@pytest.mark.parametrize( - "user,expected", - tuple(zip( - conftest.TEST_USERS, - (sorted( - {res.resource_id: res for res in - (conftest.TEST_RESOURCES_GROUP_01 + - conftest.TEST_RESOURCES_PUBLIC)}.values(), - key=lambda resource: resource.resource_id), - sorted( - {res.resource_id: res for res in - ((conftest.TEST_RESOURCES_GROUP_01[1],) + - conftest.TEST_RESOURCES_PUBLIC)}.values() - , key=lambda resource: resource.resource_id), - PUBLIC_RESOURCES, PUBLIC_RESOURCES)))) -def test_user_resources(fxtr_group_user_roles, user, expected): - """ - GIVEN: some resources in the database - WHEN: a particular user's resources are requested - THEN: list only the resources for which the user can access - """ - conn, *_others = fxtr_group_user_roles - assert sorted( - {res.resource_id: res for res in user_resources(conn, user) - }.values(), key=lambda resource: resource.resource_id) == expected diff --git a/tests/unit/auth/test_roles.py b/tests/unit/auth/test_roles.py deleted file mode 100644 index 8e22bb5..0000000 --- a/tests/unit/auth/test_roles.py +++ /dev/null @@ -1,127 +0,0 @@ -"""Test functions dealing with group management.""" -import uuid - -import pytest - -from gn3.auth import db -from gn3.auth.authorisation.privileges import Privilege -from gn3.auth.authorisation.errors import AuthorisationError -from gn3.auth.authorisation.roles.models import Role, user_roles, create_role - -from tests.unit.auth import conftest -from tests.unit.auth.fixtures import TEST_USERS - -create_role_failure = { - "status": "error", - "message": "Unauthorised: Could not create role" -} - - -def uuid_fn(): - """Mock function for uuid""" - return uuid.UUID("d32611e3-07fc-4564-b56c-786c6db6de2b") - - -PRIVILEGES = ( - Privilege("group:resource:view-resource", - "view a resource and use it in computations"), - Privilege("group:resource:edit-resource", "edit/update a resource")) - -@pytest.mark.unit_test -@pytest.mark.parametrize( - "user,expected", tuple(zip(conftest.TEST_USERS[0:1], ( - Role(uuid.UUID("d32611e3-07fc-4564-b56c-786c6db6de2b"), "a_test_role", - True, PRIVILEGES),)))) -def test_create_role(# pylint: disable=[too-many-arguments] - fxtr_app, auth_testdb_path, mocker, fxtr_users, user, expected):# pylint: disable=[unused-argument] - """ - GIVEN: an authenticated user - WHEN: the user attempts to create a role - THEN: verify they are only able to create the role if they have the - appropriate privileges - """ - mocker.patch("gn3.auth.authorisation.roles.models.uuid4", uuid_fn) - mocker.patch("gn3.auth.authorisation.checks.require_oauth.acquire", - conftest.get_tokeniser(user)) - with db.connection(auth_testdb_path) as conn, db.cursor(conn) as cursor: - the_role = create_role(cursor, "a_test_role", PRIVILEGES) - assert the_role == expected - -@pytest.mark.unit_test -@pytest.mark.parametrize( - "user,expected", tuple(zip(conftest.TEST_USERS[1:], ( - create_role_failure, create_role_failure, create_role_failure)))) -def test_create_role_raises_exception_for_unauthorised_users(# pylint: disable=[too-many-arguments] - fxtr_app, auth_testdb_path, mocker, fxtr_users, user, expected):# pylint: disable=[unused-argument] - """ - GIVEN: an authenticated user - WHEN: the user attempts to create a role - THEN: verify they are only able to create the role if they have the - appropriate privileges - """ - mocker.patch("gn3.auth.authorisation.roles.models.uuid4", uuid_fn) - mocker.patch("gn3.auth.authorisation.checks.require_oauth.acquire", - conftest.get_tokeniser(user)) - with db.connection(auth_testdb_path) as conn, db.cursor(conn) as cursor: - with pytest.raises(AuthorisationError): - create_role(cursor, "a_test_role", PRIVILEGES) - -@pytest.mark.unit_test -@pytest.mark.parametrize( - "user,expected", - (zip(TEST_USERS, - ((Role( - role_id=uuid.UUID('a0e67630-d502-4b9f-b23f-6805d0f30e30'), - role_name='group-leader', user_editable=False, - privileges=( - Privilege(privilege_id='group:resource:create-resource', - privilege_description='Create a resource object'), - Privilege(privilege_id='group:resource:delete-resource', - privilege_description='Delete a resource'), - Privilege(privilege_id='group:resource:edit-resource', - privilege_description='edit/update a resource'), - Privilege( - privilege_id='group:resource:view-resource', - privilege_description=( - 'view a resource and use it in computations')), - Privilege(privilege_id='group:role:create-role', - privilege_description='Create a new role'), - Privilege(privilege_id='group:role:delete-role', - privilege_description='Delete an existing role'), - Privilege(privilege_id='group:role:edit-role', - privilege_description='edit/update an existing role'), - Privilege(privilege_id='group:user:add-group-member', - privilege_description='Add a user to a group'), - Privilege(privilege_id='group:user:assign-role', - privilege_description=( - 'Assign a role to an existing user')), - Privilege(privilege_id='group:user:remove-group-member', - privilege_description='Remove a user from a group'), - Privilege(privilege_id='system:group:delete-group', - privilege_description='Delete a group'), - Privilege(privilege_id='system:group:edit-group', - privilege_description='Edit the details of a group'), - Privilege( - privilege_id='system:group:transfer-group-leader', - privilege_description=( - 'Transfer leadership of the group to some other ' - 'member')), - Privilege(privilege_id='system:group:view-group', - privilege_description='View the details of a group'), - Privilege(privilege_id='system:user:list', - privilege_description='List users in the system'))), - Role( - role_id=uuid.UUID("ade7e6b0-ba9c-4b51-87d0-2af7fe39a347"), - role_name="group-creator", user_editable=False, - privileges=( - Privilege(privilege_id='system:group:create-group', - privilege_description = "Create a group"),))), - tuple(), tuple(), tuple())))) -def test_user_roles(fxtr_group_user_roles, user, expected): - """ - GIVEN: an authenticated user - WHEN: we request the user's privileges - THEN: return **ALL** the privileges attached to the user - """ - conn, *_others = fxtr_group_user_roles - assert user_roles(conn, user) == expected diff --git a/tests/unit/auth/test_token.py b/tests/unit/auth/test_token.py deleted file mode 100644 index 76316ea..0000000 --- a/tests/unit/auth/test_token.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Test the OAuth2 authorisation""" - -import pytest - -from gn3.auth import db - -SUCCESS_RESULT = { - "status_code": 200, - "result": { - "access_token": "123456ABCDE", - "expires_in": 864000, - "scope": "profile", - "token_type": "Bearer"}} - -USERNAME_PASSWORD_FAIL_RESULT = { - "status_code": 400, - "result": { - 'error': 'invalid_request', - 'error_description': 'Invalid "username" or "password" in request.'}} - -def gen_token(client, grant_type, user, scope): # pylint: disable=[unused-argument] - """Generate tokens for tests""" - return "123456ABCDE" - -@pytest.mark.unit_test -@pytest.mark.parametrize( - "test_data,expected", - ((("group@lead.er", "password_for_user_001", 0), SUCCESS_RESULT), - (("group@mem.ber01", "password_for_user_002", 1), SUCCESS_RESULT), - (("group@mem.ber02", "password_for_user_003", 2), SUCCESS_RESULT), - (("unaff@iliated.user", "password_for_user_004", 3), SUCCESS_RESULT), - (("group@lead.er", "brrr", 0), USERNAME_PASSWORD_FAIL_RESULT), - (("group@mem.ber010", "password_for_user_002", 1), USERNAME_PASSWORD_FAIL_RESULT), - (("papa", "yada", 2), USERNAME_PASSWORD_FAIL_RESULT), - # (("unaff@iliated.user", "password_for_user_004", 1), USERNAME_PASSWORD_FAIL_RESULT) - )) -def test_token(fxtr_app, fxtr_oauth2_clients, test_data, expected): - """ - GIVEN: a registered oauth2 client, a user - WHEN: a token is requested via the 'password' grant - THEN: check that: - a) when email and password are valid, we get a token back - b) when either email or password or both are invalid, we get error message - back - c) TODO: when user tries to use wrong client, we get error message back - """ - conn, oa2clients = fxtr_oauth2_clients - email, password, client_idx = test_data - data = { - "grant_type": "password", "scope": "profile nonexistent-scope", - "client_id": oa2clients[client_idx].client_id, - "client_secret": oa2clients[client_idx].client_secret, - "username": email, "password": password} - - with fxtr_app.test_client() as client, db.cursor(conn) as cursor: - res = client.post("/api/oauth2/token", data=data) - # cleanup db - cursor.execute("DELETE FROM oauth2_tokens WHERE access_token=?", - (gen_token(None, None, None, None),)) - assert res.status_code == expected["status_code"] - for key in expected["result"]: - assert res.json[key] == expected["result"][key] |