diff options
author | Frederick Muriuki Muriithi | 2023-03-15 05:03:16 +0300 |
---|---|---|
committer | Frederick Muriuki Muriithi | 2023-03-15 05:03:16 +0300 |
commit | 47f56fb88604e4f0aa43cf9d75db7570f18ff5f7 (patch) | |
tree | 2826bda23cb955d5b1d644f570abc9a578da8473 /gn3 | |
parent | 616560ada0104f148d9cb54405122a3e6a3d6ea1 (diff) | |
download | genenetwork3-47f56fb88604e4f0aa43cf9d75db7570f18ff5f7.tar.gz |
auth: Data Migrations: Rework migration trigger
Rework the migration trigger endpoint such that it does a bulk import of all
the resources in the redis store.
It also expects that the user that triggers the migration have the appropriate
privileges.
Diffstat (limited to 'gn3')
-rw-r--r-- | gn3/auth/authorisation/data/views.py | 263 |
1 files changed, 104 insertions, 159 deletions
diff --git a/gn3/auth/authorisation/data/views.py b/gn3/auth/authorisation/data/views.py index 03c32b3..47c3fe8 100644 --- a/gn3/auth/authorisation/data/views.py +++ b/gn3/auth/authorisation/data/views.py @@ -2,9 +2,11 @@ import os import uuid import json +import random +import string import datetime -from typing import Sequence from functools import reduce +from typing import Sequence, Iterable import redis from email_validator import validate_email, EmailNotValidError @@ -19,9 +21,6 @@ from gn3.auth.dictify import dictify from gn3.auth.authorisation.errors import NotFoundError -from gn3.auth.authorisation.users.views import ( - validate_password, validate_username) - from gn3.auth.authorisation.roles.models import( revoke_user_role_by_name, assign_user_role_by_name) @@ -33,15 +32,11 @@ 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.authorisation.errors import ForbiddenAccess, AuthorisationError +from gn3.auth.authorisation.errors import ForbiddenAccess from gn3.auth.authentication.oauth2.resource_server import require_oauth -from gn3.auth.authentication.users import User, user_by_id, set_user_password -from gn3.auth.authentication.oauth2.models.oauth2token import ( - OAuth2Token, save_token, revoke_token) -from gn3.auth.authentication.oauth2.models.oauth2client import ( - client_by_id_and_secret) +from gn3.auth.authentication.users import User, user_by_email, set_user_password data = Blueprint("data", __name__) @@ -119,8 +114,9 @@ def migrate_user_group(conn: db.DbConnection, user: User) -> Group: group = user_group(conn, user).maybe(# type: ignore[misc] False, lambda grp: grp) # type: ignore[arg-type] if not bool(group): - group = Group(uuid.uuid4(), f"{user.name}'s Group", { - "created": datetime.datetime.now().isoformat(), + now = datetime.datetime.now().isoformat() + group = Group(uuid.uuid4(), f"{user.name}'s Group ({now})", { + "created": now, "notes": "Imported from redis" }) with db.cursor(conn) as cursor: @@ -160,18 +156,21 @@ def __parametrise__(group: Group, datasets: Sequence[dict], "accession_id": dataset["accession_id"] } for dataset in datasets) -def user_redis_resources(rconn: redis.Redis, user_id: uuid.UUID) -> tuple[ - tuple[dict], tuple[dict], tuple[dict]]: - """Acquire any resources from redis.""" - return reduce(# type: ignore[return-value] - __redis_datasets_by_type__, - (dataset for dataset in - (dataset for _key,dataset in { - key: json.loads(val) - for key,val in rconn.hgetall("resources").items() - }.items()) - if dataset["owner_id"] == str(user_id)), - (tuple(), tuple(), tuple())) +def __org_by_user_id__(acc, resource): + try: + user_id = uuid.UUID(resource["owner_id"]) + return { + **acc, + user_id: acc.get(user_id, tuple()) + (resource,) + } + except ValueError as _verr: + return acc + +def redis_resources(rconn: redis.Redis) -> Iterable[dict[str, str]]: + """Retrieve ALL defined resources from Redis""" + return ( + json.loads(resource) + for resource in rconn.hgetall("resources").values()) def system_admin_user(conn: db.DbConnection) -> User: """Return a system admin user.""" @@ -186,41 +185,45 @@ def system_admin_user(conn: db.DbConnection) -> User: rows[0]["name"]) raise NotFoundError("Could not find a system administration user.") -def generate_sysadmin_token() -> OAuth2Token: - """Generate a token for a user with system administration privileges.""" - db_uri = app.config["AUTH_DB"] - server = app.config["OAUTH2_SERVER"] - with (require_oauth.acquire("profile") as the_token, - db.connection(db_uri) as conn): - admin_user = system_admin_user(conn) - token = OAuth2Token( - token_id = uuid.uuid4(), - client = the_token.client, - **server.generate_token( - client = the_token.client, - grant_type = "implicit", - user = admin_user, - scope = the_token.scope, - expires_in = 30*60, - include_refresh_token=False), - refresh_token = None, - revoked=False, - issued_at=datetime.datetime.now(), - user=admin_user) - save_token(conn, token) - return token - -def migrate_data( - authconn: db.DbConnection, gn3conn: gn3db.Connection, - redis_resources: tuple[tuple[dict], tuple[dict], tuple[dict]], - group: Group) -> tuple[dict[str, str], ...]: +def migrate_user( + conn: db.DbConnection, email: str, username: str, password: str) -> User: + """Migrate the user, if not already migrated.""" + try: + return user_by_email(conn, email) + except NotFoundError as _nfe: + user = User(uuid.uuid4(), email, username) + with db.cursor(conn) as cursor: + cursor.execute( + "INSERT INTO users(user_id, email, name) " + "VALUES (?, ?, ?)", + (str(user.user_id), user.email, user.name)) + set_user_password(cursor, user, password) + return user + +def __generate_random_password__(length: int = 25): + """Generate a random password string""" + return "".join(random.choices( + string.ascii_letters + string.punctuation + string.digits, + k=length)) + +def migrate_data(# pylint: disable=[too-many-locals] + authconn: db.DbConnection, + gn3conn: gn3db.Connection, + rconn: redis.Redis, + redis_user_id: uuid.UUID, + redisresources: tuple[dict[str, str], ...]) -> tuple[ + User, Group, tuple[dict[str, str], ...]]: """Migrate data attached to the user to the user's group.""" - redis_mrna, redis_geno, redis_pheno = redis_resources - ## BEGIN: Escalate privileges temporarily to enable fetching of data - ## ===================================== - new_token = generate_sysadmin_token() - with app.test_request_context(headers={ - "Authorization": f"Bearer {new_token.access_token}"}): + try: + user_details = json.loads(rconn.hget("users", str(redis_user_id))) + email = validate_email(user_details["email_address"]) + user = migrate_user(authconn, email["email"], + user_details.get("full_name") or "NOT SET", + __generate_random_password__()) + group = migrate_user_group(authconn, user) + redis_mrna, redis_geno, redis_pheno = reduce(#type: ignore[var-annotated] + __redis_datasets_by_type__, redisresources, + (tuple(), tuple(), tuple())) mrna_datasets = __unmigrated_data__( retrieve_ungrouped_data(authconn, gn3conn, "mrna"), redis_mrna) geno_datasets = __unmigrated_data__( @@ -228,79 +231,27 @@ def migrate_data( pheno_datasets = __unmigrated_data__( retrieve_ungrouped_data(authconn, gn3conn, "phenotype"), redis_pheno) - save_token(authconn, revoke_token(new_token)) - ## ===================================== - ## END: Escalate privileges temporarily to enable fetching of data - - params = ( - __parametrise__(group, mrna_datasets, "mRNA") + - __parametrise__(group, geno_datasets, "Genotype") + - __parametrise__(group, pheno_datasets, "Phenotype")) - if len(params) > 0: - with db.cursor(authconn) as cursor: - cursor.executemany( - "INSERT INTO linked_group_data VALUES" - "(:group_id, :dataset_type, :dataset_or_trait_id, " - ":dataset_name, :dataset_fullname, :accession_id)", - params) - - return params - -@data.route("/user/migrate", methods=["POST"]) -# @require_oauth("migrate-data") -def migrate_user() -> Response: - """Migrate the user""" - def __migrate_user__(conn: db.DbConnection, user_id: uuid.UUID, email: str, - username: str, password: str) -> User: - """Migrate the user, if not already migrated.""" - try: - return user_by_id(conn, user_id) - except NotFoundError as _nfe: - user = User(user_id, email, username) - with db.cursor(conn) as cursor: - cursor.execute( - "INSERT INTO users(user_id, email, name) " - "VALUES (?, ?, ?)", - (str(user.user_id), user.email, user.name)) - set_user_password(cursor, user, password) - return user - try: - db_uri = app.config.get("AUTH_DB", "").strip() - with (db.connection(db_uri) as authconn, - redis.Redis(decode_responses=True) as rconn): - client_id = uuid.UUID(request.form.get("client_id")) - client_secret = request.form.get("client_secret", "NOTASECRET") - client = client_by_id_and_secret(authconn, client_id, client_secret) - authorised_clients = app.config.get( - "OAUTH2_CLIENTS_WITH_DATA_MIGRATION_PRIVILEGE", []) - if client.client_id not in authorised_clients: - raise ForbiddenAccess("You cannot access this endpoint.") - - user_id = uuid.UUID(request.form.get("user_id")) - redis_dets = rconn.hget("users", str(user_id)) - user_details = json.loads(redis_dets) - if user_details: - email = validate_email(user_details["email_address"]) - user = __migrate_user__( - authconn, user_id, email["email"], - validate_username(user_details.get("full_name", "")), - validate_password( - request.form.get("password", ""), - request.form.get("confirm_password", ""))) - return jsonify({ - "user": dictify(user), - "description": "Successfully migrated user." - }) - raise NotFoundError( - f"No user with ID '{user_id}'") - except EmailNotValidError as enve: - raise AuthorisationError(f"Email Error: {str(enve)}") from enve - except ValueError as verr: - raise AuthorisationError(verr.args[0]) from verr - -@data.route("/user/<uuid:user_id>/migrate", methods=["POST"]) + params = ( + __parametrise__(group, mrna_datasets, "mRNA") + + __parametrise__(group, geno_datasets, "Genotype") + + __parametrise__(group, pheno_datasets, "Phenotype")) + if len(params) > 0: + with db.cursor(authconn) as cursor: + cursor.executemany( + "INSERT INTO linked_group_data VALUES" + "(:group_id, :dataset_type, :dataset_or_trait_id, " + ":dataset_name, :dataset_fullname, :accession_id)", + params) + + return user, group, params + except EmailNotValidError as _enve: + pass + + return tuple() # type: ignore[return-value] + +@data.route("/users/migrate", methods=["POST"]) @require_oauth("migrate-data") -def migrate_user_data(user_id: uuid.UUID) -> Response: +def migrate_users_data() -> Response: """ Special, protected endpoint to enable the migration of data from the older system to the newer system with groups, resources and privileges. @@ -312,30 +263,30 @@ def migrate_user_data(user_id: uuid.UUID) -> Response: if bool(db_uri) and os.path.exists(db_uri): authorised_clients = app.config.get( "OAUTH2_CLIENTS_WITH_DATA_MIGRATION_PRIVILEGE", []) - with require_oauth.acquire("migrate-data") as the_token: + with (require_oauth.acquire("migrate-data") as the_token, + db.connection(db_uri) as authconn, + redis.Redis(decode_responses=True) as rconn, + gn3db.database_connection() as gn3conn): if the_token.client.client_id in authorised_clients: - user = the_token.user - if not user_id == user.user_id: - raise AuthorisationError( - "You cannot trigger migration of another user's data.") - with (db.connection(db_uri) as authconn, - redis.Redis(decode_responses=True) as rconn, - gn3db.database_connection() as gn3conn): - redis_resources = user_redis_resources(rconn, user.user_id) - user_resource_data: tuple = tuple() - if any(bool(item) for item in redis_resources): - group = migrate_user_group(authconn, user) - user_resource_data = migrate_data( - authconn, gn3conn, redis_resources, group) - ## TODO: Maybe delete user from redis... - return jsonify({ - "description": ( - f"Migrated {len(user_resource_data)} resource data " - "items."), - "user": dictify(user), - "group": dictify(group) - }) - + by_user: dict[uuid.UUID, tuple[dict[str, str], ...]] = reduce( + __org_by_user_id__, redis_resources(rconn), {}) + users, groups, resource_data_params = reduce(# type: ignore[var-annotated, arg-type] + lambda acc, ugp: (acc[0] + (ugp[0],),# type: ignore[return-value, arg-type] + acc[1] + (ugp[1],), + acc[2] + ugp[2]), + ( + migrate_data( + authconn, gn3conn, rconn, user_id, user_resources) + for user_id, user_resources in by_user.items()), + (tuple(), tuple(), tuple())) + return jsonify({ + "description": ( + f"Migrated {len(resource_data_params)} resource data " + f"items for {len(users)} users into {len(groups)} " + "groups."), + "users": tuple(dictify(user) for user in users), + "groups": tuple(dictify(group) for group in groups) + }) raise ForbiddenAccess("You cannot access this endpoint.") return app.response_class( @@ -345,9 +296,3 @@ def migrate_user_data(user_id: uuid.UUID) -> Response: "The data migration service is currently unavailable.") }), status=500, mimetype="application/json") - - # return jsonify({ - # "error": "Unavailable", - # "error_description": ( - # "The data migration service is currently unavailable.") - # }), 503 |