aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gn_auth/auth/authorisation/resources/inbredset/models.py96
-rw-r--r--gn_auth/auth/authorisation/resources/inbredset/views.py82
-rw-r--r--gn_auth/auth/authorisation/resources/models.py54
-rw-r--r--gn_auth/auth/authorisation/resources/views.py16
-rw-r--r--gn_auth/auth/authorisation/users/collections/models.py4
-rw-r--r--gn_auth/auth/authorisation/users/collections/views.py1
-rw-r--r--gn_auth/auth/authorisation/users/views.py35
-rw-r--r--gn_auth/auth/views.py2
-rw-r--r--gn_auth/templates/users/change-password.html52
-rw-r--r--tests/unit/auth/test_resources.py17
10 files changed, 316 insertions, 43 deletions
diff --git a/gn_auth/auth/authorisation/resources/inbredset/models.py b/gn_auth/auth/authorisation/resources/inbredset/models.py
new file mode 100644
index 0000000..de1c18a
--- /dev/null
+++ b/gn_auth/auth/authorisation/resources/inbredset/models.py
@@ -0,0 +1,96 @@
+"""Functions to handle the low-level details regarding populations auth."""
+from uuid import UUID, uuid4
+
+import sqlite3
+
+from gn_auth.auth.errors import NotFoundError
+from gn_auth.auth.authentication.users import User
+from gn_auth.auth.authorisation.resources.groups.models import Group
+from gn_auth.auth.authorisation.resources.base import Resource, ResourceCategory
+from gn_auth.auth.authorisation.resources.models import (
+ create_resource as _create_resource)
+
+def create_resource(
+ cursor: sqlite3.Cursor,
+ resource_name: str,
+ user: User,
+ group: Group,
+ public: bool
+) -> Resource:
+ """Convenience function to create a resource of type 'inbredset-group'."""
+ cursor.execute("SELECT * FROM resource_categories "
+ "WHERE resource_category_key='inbredset-group'")
+ category = cursor.fetchone()
+ if category:
+ return _create_resource(cursor,
+ resource_name,
+ ResourceCategory(
+ resource_category_id=UUID(
+ category["resource_category_id"]),
+ resource_category_key="inbredset-group",
+ resource_category_description=category[
+ "resource_category_description"]),
+ user,
+ group,
+ public)
+ raise NotFoundError("Could not find a 'inbredset-group' resource category.")
+
+
+def assign_inbredset_group_owner_role(
+ cursor: sqlite3.Cursor,
+ resource: Resource,
+ user: User
+) -> Resource:
+ """
+ Assign `user` as `InbredSet Group Owner` is resource category is
+ 'inbredset-group'.
+ """
+ if resource.resource_category.resource_category_key == "inbredset-group":
+ cursor.execute(
+ "SELECT * FROM roles WHERE role_name='inbredset-group-owner'")
+ role = cursor.fetchone()
+ cursor.execute(
+ "INSERT INTO user_roles "
+ "VALUES(:user_id, :role_id, :resource_id) "
+ "ON CONFLICT (user_id, role_id, resource_id) DO NOTHING",
+ {
+ "user_id": str(user.user_id),
+ "role_id": str(role["role_id"]),
+ "resource_id": str(resource.resource_id)
+ })
+
+ return resource
+
+
+def link_data_to_resource(# pylint: disable=[too-many-arguments]
+ cursor: sqlite3.Cursor,
+ resource_id: UUID,
+ species_id: int,
+ population_id: int,
+ population_name: str,
+ population_fullname: str
+) -> dict:
+ """Link a species population to a resource for auth purposes."""
+ params = {
+ "resource_id": str(resource_id),
+ "data_link_id": str(uuid4()),
+ "species_id": species_id,
+ "population_id": population_id,
+ "population_name": population_name,
+ "population_fullname": population_fullname
+ }
+ cursor.execute(
+ "INSERT INTO linked_inbredset_groups "
+ "VALUES("
+ " :data_link_id,"
+ " :species_id,"
+ " :population_id,"
+ " :population_name,"
+ " :population_fullname"
+ ")",
+ params)
+ cursor.execute(
+ "INSERT INTO inbredset_group_resources "
+ "VALUES (:resource_id, :data_link_id)",
+ params)
+ return params
diff --git a/gn_auth/auth/authorisation/resources/inbredset/views.py b/gn_auth/auth/authorisation/resources/inbredset/views.py
index 444c442..b559105 100644
--- a/gn_auth/auth/authorisation/resources/inbredset/views.py
+++ b/gn_auth/auth/authorisation/resources/inbredset/views.py
@@ -1,12 +1,22 @@
"""Views for InbredSet resources."""
-from flask import jsonify, Response, Blueprint
+from pymonad.either import Left, Right, Either
+from flask import jsonify, Response, Blueprint, current_app as app
+
from gn_auth.auth.db import sqlite3 as db
+from gn_auth.auth.requests import request_json
from gn_auth.auth.db.sqlite3 import with_db_connection
+from gn_auth.auth.authentication.oauth2.resource_server import require_oauth
+from gn_auth.auth.authorisation.resources.groups.models import user_group
+
+from .models import (create_resource,
+ link_data_to_resource,
+ assign_inbredset_group_owner_role)
-iset = Blueprint("inbredset", __name__)
+popbp = Blueprint("populations", __name__)
-@iset.route("/resource-id/<int:speciesid>/<int:inbredsetid>")
+@popbp.route("/populations/resource-id/<int:speciesid>/<int:inbredsetid>",
+ methods=["GET"])
def resource_id_by_inbredset_id(speciesid: int, inbredsetid: int) -> Response:
"""Retrieve the resource ID for resource attached to the inbredset."""
def __res_by_iset_id__(conn):
@@ -34,3 +44,69 @@ def resource_id_by_inbredset_id(speciesid: int, inbredsetid: int) -> Response:
resp.status_code = 404
return resp
+
+
+@popbp.route("/populations/create", methods=["POST"])
+@require_oauth("profile group resource")
+def create_population_resource():
+ """Create a resource of type 'inbredset-group'."""
+ with (require_oauth.acquire("profile group resource") as _token,
+ db.connection(app.config["AUTH_DB"]) as conn,
+ db.cursor(conn) as cursor):
+
+ def __check_form__(form, usergroup) -> Either:
+ """Check form for errors."""
+ errors: tuple[str, ...] = tuple()
+
+ species_id = form.get("species_id")
+ if not bool(species_id):
+ errors = errors + ("Missing `species_id` value.",)
+
+ population_id = form.get("population_id")
+ if not bool(population_id):
+ errors = errors + ("Missing `population_id` value.",)
+
+ population_name = form.get("population_name")
+ if not bool(population_name):
+ errors = errors + ("Missing `population_name` value.",)
+
+ population_fullname = form.get("population_fullname")
+ if not bool(population_fullname):
+ errors = errors + ("Missing `population_fullname` value.",)
+
+ if bool(errors):
+ error_messages = "\n\t - ".join(errors)
+ return Left({
+ "error": "Invalid Request Data!",
+ "error_description": error_messages
+ })
+
+ return Right({"formdata": form, "group": usergroup})
+
+ return user_group(conn, _token.user).then(
+ lambda group: __check_form__(request_json(), group)
+ ).then(
+ lambda formdata: {
+ **formdata,
+ "resource": create_resource(
+ cursor,
+ f"Population — {formdata['formdata']['population_name']}",
+ _token.user,
+ formdata["group"],
+ formdata["formdata"].get("public", "on") == "on")}
+ ).then(
+ lambda resource: {
+ **resource,
+ "resource": assign_inbredset_group_owner_role(
+ cursor, resource["resource"], _token.user)}
+ ).then(
+ lambda resource: link_data_to_resource(
+ cursor,
+ resource["resource"].resource_id,
+ resource["formdata"]["species_id"],
+ resource["formdata"]["population_id"],
+ resource["formdata"]["population_name"],
+ resource["formdata"]["population_fullname"])
+ ).either(
+ lambda error: (jsonify(error), 400),
+ jsonify)
diff --git a/gn_auth/auth/authorisation/resources/models.py b/gn_auth/auth/authorisation/resources/models.py
index c7c8352..fa7797b 100644
--- a/gn_auth/auth/authorisation/resources/models.py
+++ b/gn_auth/auth/authorisation/resources/models.py
@@ -17,7 +17,7 @@ from gn_auth.auth.errors import NotFoundError, AuthorisationError
from .checks import authorised_for
from .base import Resource, ResourceCategory
-from .groups.models import Group, user_group, is_group_leader
+from .groups.models import Group, is_group_leader
from .mrna import (
resource_data as mrna_resource_data,
attach_resources_data as mrna_attach_resources_data,
@@ -34,8 +34,6 @@ from .phenotype import (
link_data_to_resource as phenotype_link_data_to_resource,
unlink_data_from_resource as phenotype_unlink_data_from_resource)
-from .errors import MissingGroupError
-
def __assign_resource_owner_role__(cursor, resource, user):
"""Assign `user` the 'Resource Owner' role for `resource`."""
cursor.execute("SELECT * FROM roles WHERE role_name='resource-owner'")
@@ -66,28 +64,36 @@ def resource_from_dbrow(row: sqlite3.Row):
@authorised_p(("group:resource:create-resource",),
error_description="Insufficient privileges to create a resource",
oauth2_scope="profile resource")
-def create_resource(
- conn: db.DbConnection, resource_name: str,
- resource_category: ResourceCategory, user: User,
- public: bool) -> Resource:
+def create_resource(# pylint: disable=[too-many-arguments]
+ cursor: sqlite3.Cursor,
+ resource_name: str,
+ resource_category: ResourceCategory,
+ user: User,
+ group: Group,
+ public: bool
+) -> Resource:
"""Create a resource item."""
- with db.cursor(conn) as cursor:
- group = user_group(conn, user).maybe(
- False, lambda grp: grp)# type: ignore[misc, arg-type]
- if not group:
- raise MissingGroupError(# Not all resources require an owner group
- "User with no group cannot create a resource.")
- resource = Resource(uuid4(), resource_name, resource_category, public)
- cursor.execute(
- "INSERT INTO resources VALUES (?, ?, ?, ?)",
- (str(resource.resource_id),
- resource_name,
- str(resource.resource_category.resource_category_id),
- 1 if resource.public else 0))
- cursor.execute("INSERT INTO resource_ownership (group_id, resource_id) "
- "VALUES (?, ?)",
- (str(group.group_id), str(resource.resource_id)))
- __assign_resource_owner_role__(cursor, resource, user)
+ resource = Resource(uuid4(), resource_name, resource_category, public)
+ cursor.execute(
+ "INSERT INTO resources VALUES (?, ?, ?, ?)",
+ (str(resource.resource_id),
+ resource_name,
+ str(resource.resource_category.resource_category_id),
+ 1 if resource.public else 0))
+ # TODO: @fredmanglis,@rookie101
+ # 1. Move the actions below into a (the?) hooks system
+ # 2. Do more checks: A resource can have varying hooks depending on type
+ # e.g. if mRNA, pheno or geno resource, assign:
+ # - "resource-owner"
+ # if inbredset-group, assign:
+ # - "resource-owner",
+ # - "inbredset-group-owner" etc.
+ # if resource is of type "group", assign:
+ # - group-leader
+ cursor.execute("INSERT INTO resource_ownership (group_id, resource_id) "
+ "VALUES (?, ?)",
+ (str(group.group_id), str(resource.resource_id)))
+ __assign_resource_owner_role__(cursor, resource, user)
return resource
diff --git a/gn_auth/auth/authorisation/resources/views.py b/gn_auth/auth/authorisation/resources/views.py
index 494fde9..3f972f6 100644
--- a/gn_auth/auth/authorisation/resources/views.py
+++ b/gn_auth/auth/authorisation/resources/views.py
@@ -40,15 +40,18 @@ from gn_auth.auth.authentication.oauth2.resource_server import require_oauth
from gn_auth.auth.authentication.users import User, user_by_id, user_by_email
from .checks import authorised_for
+from .inbredset.views import popbp
+from .errors import MissingGroupError
+from .groups.models import Group, user_group
from .models import (
Resource, resource_data, resource_by_id, public_resources,
resource_categories, assign_resource_user, link_data_to_resource,
unassign_resource_user, resource_category_by_id, user_roles_on_resources,
unlink_data_from_resource, create_resource as _create_resource,
get_resource_id)
-from .groups.models import Group
resources = Blueprint("resources", __name__)
+resources.register_blueprint(popbp, url_prefix="/")
@resources.route("/categories", methods=["GET"])
@require_oauth("profile group resource")
@@ -68,13 +71,20 @@ def create_resource() -> Response:
resource_name = form.get("resource_name")
resource_category_id = UUID(form.get("resource_category"))
db_uri = app.config["AUTH_DB"]
- with db.connection(db_uri) as conn:
+ with (db.connection(db_uri) as conn,
+ db.cursor(conn) as cursor):
try:
+ group = user_group(conn, the_token.user).maybe(
+ False, lambda grp: grp)# type: ignore[misc, arg-type]
+ if not group:
+ raise MissingGroupError(# Not all resources require an owner group
+ "User with no group cannot create a resource.")
resource = _create_resource(
- conn,
+ cursor,
resource_name,
resource_category_by_id(conn, resource_category_id),
the_token.user,
+ group,
(form.get("public") == "on"))
return jsonify(asdict(resource))
except sqlite3.IntegrityError as sql3ie:
diff --git a/gn_auth/auth/authorisation/users/collections/models.py b/gn_auth/auth/authorisation/users/collections/models.py
index b4a24f3..f0a7fa2 100644
--- a/gn_auth/auth/authorisation/users/collections/models.py
+++ b/gn_auth/auth/authorisation/users/collections/models.py
@@ -205,8 +205,10 @@ def add_traits(rconn: Redis,
mod_col = tuple(coll for coll in ucolls if coll["id"] == collection_id)
__raise_if_not_single_collection__(user, collection_id, mod_col)
new_members = tuple(set(tuple(mod_col[0]["members"]) + traits))
+ now = datetime.utcnow()
new_coll = {
**mod_col[0],
+ "changed": now,
"members": new_members,
"num_members": len(new_members)
}
@@ -233,8 +235,10 @@ def remove_traits(rconn: Redis,
__raise_if_not_single_collection__(user, collection_id, mod_col)
new_members = tuple(
trait for trait in mod_col[0]["members"] if trait not in traits)
+ now = datetime.utcnow()
new_coll = {
**mod_col[0],
+ "changed": now,
"members": new_members,
"num_members": len(new_members)
}
diff --git a/gn_auth/auth/authorisation/users/collections/views.py b/gn_auth/auth/authorisation/users/collections/views.py
index eeae91d..f619c3d 100644
--- a/gn_auth/auth/authorisation/users/collections/views.py
+++ b/gn_auth/auth/authorisation/users/collections/views.py
@@ -113,6 +113,7 @@ def import_anonymous() -> Response:
anon_id = UUID(request.json.get("anon_id"))#type: ignore[union-attr]
anon_colls = user_collections(redisconn, User(
anon_id, "anon@ymous.user", "Anonymous User"))
+ anon_colls = tuple(coll for coll in anon_colls if coll['num_members'] > 0)
save_collections(
redisconn,
token.user,
diff --git a/gn_auth/auth/authorisation/users/views.py b/gn_auth/auth/authorisation/users/views.py
index 4cd498c..7adcd06 100644
--- a/gn_auth/auth/authorisation/users/views.py
+++ b/gn_auth/auth/authorisation/users/views.py
@@ -152,8 +152,8 @@ def send_verification_email(
timedelta(
minutes=expiration_minutes)).timestamp())
})
- send_message(smtp_user=current_app.config["SMTP_USER"],
- smtp_passwd=current_app.config["SMTP_PASSWORD"],
+ send_message(smtp_user=current_app.config.get("SMTP_USER", ""),
+ smtp_passwd=current_app.config.get("SMTP_PASSWORD", ""),
message=build_email_message(
from_address=current_app.config["EMAIL_ADDRESS"],
to_addresses=(user_address(user),),
@@ -435,8 +435,7 @@ def forgot_password():
flash("You MUST provide an email.", "alert-danger")
return redirect(url_for("oauth2.users.forgot_password"))
- with (db.connection(current_app.config["AUTH_DB"]) as conn,
- db.cursor(conn) as cursor):
+ with db.connection(current_app.config["AUTH_DB"]) as conn:
user = user_by_email(conn, form["email"])
if not bool(user):
flash("We could not find an account with that email.",
@@ -467,8 +466,8 @@ def change_password(forgot_password_token):
"SELECT fpt.*, u.email FROM forgot_password_tokens AS fpt "
"INNER JOIN users AS u ON fpt.user_id=u.user_id WHERE token=?",
(forgot_password_token,))
+ token = cursor.fetchone()
if request.method == "GET":
- token = cursor.fetchone()
if bool(token):
return render_template(
"users/change-password.html",
@@ -480,4 +479,28 @@ def change_password(forgot_password_token):
flash("Invalid Token: We cannot change your password!",
"alert-danger")
return login_page
- return "Do actual password change..."
+
+ password = request.form["password"]
+ confirm_password = request.form["confirm-password"]
+ change_password_page = redirect(url_for(
+ "oauth2.users.change_password",
+ client_id=request.args["client_id"],
+ redirect_uri=request.args["redirect_uri"],
+ response_type=request.args["response_type"],
+ forgot_password_token=forgot_password_token))
+ if bool(password) and bool(confirm_password):
+ if password == confirm_password:
+ _user, _hashed_password = set_user_password(
+ cursor, user_by_email(conn, token["email"]), password)
+ cursor.execute(
+ "DELETE FROM forgot_password_tokens WHERE token=?",
+ (forgot_password_token,))
+ flash("Password changed successfully!", "alert-success")
+ return login_page
+
+ flash("Passwords do not match!", "alert-danger")
+ return change_password_page
+
+ flash("Both the password and its confirmation MUST be provided!",
+ "alert-danger")
+ return change_password_page
diff --git a/gn_auth/auth/views.py b/gn_auth/auth/views.py
index 17fc94b..6867f38 100644
--- a/gn_auth/auth/views.py
+++ b/gn_auth/auth/views.py
@@ -11,7 +11,6 @@ from .authorisation.resources.views import resources
from .authorisation.privileges.views import privileges
from .authorisation.resources.groups.views import groups
from .authorisation.resources.system.views import system
-from .authorisation.resources.inbredset.views import iset
oauth2 = Blueprint("oauth2", __name__)
@@ -24,4 +23,3 @@ oauth2.register_blueprint(groups, url_prefix="/group")
oauth2.register_blueprint(system, url_prefix="/system")
oauth2.register_blueprint(resources, url_prefix="/resource")
oauth2.register_blueprint(privileges, url_prefix="/privileges")
-oauth2.register_blueprint(iset, url_prefix="/resource/inbredset")
diff --git a/gn_auth/templates/users/change-password.html b/gn_auth/templates/users/change-password.html
new file mode 100644
index 0000000..f328255
--- /dev/null
+++ b/gn_auth/templates/users/change-password.html
@@ -0,0 +1,52 @@
+{%extends "base.html"%}
+
+{%block title%}gn-auth: Change Password{%endblock%}
+
+{%block pagetitle%}Change Password{%endblock%}
+
+{%block content%}
+{{flash_messages()}}
+
+<div class="container-fluid">
+ <div class="row"><h1>Change Password</h1></div>
+
+ <div class="row">
+ <form method="POST"
+ action="{{url_for('oauth2.users.change_password',
+ client_id=client_id,
+ redirect_uri=redirect_uri,
+ response_type=response_type,
+ forgot_password_token=forgot_password_token)}}">
+ <div class="form-group">
+ <p class="form-text text-info">
+ Change the password for your account with the email
+ "<strong>{{email}}</strong>".
+ </p>
+ </div>
+
+ <div class="form-group">
+ <label for="txt-password" class="form-label">New Password</label>
+ <input type="password"
+ id="txt-password"
+ name="password"
+ class="form-control"
+ required="required" />
+ </div>
+
+ <div class="form-group">
+ <label for="txt-confirm" class="form-label">Confirm New Password</label>
+ <input type="password"
+ id="txt-confirm"
+ name="confirm-password"
+ class="form-control"
+ required="required" />
+ </div>
+
+ <div class="form-group">
+ <input type="submit" class="btn btn-danger" value="change password" />
+ </div>
+ </form>
+ </div>
+
+</div>
+{%endblock%}
diff --git a/tests/unit/auth/test_resources.py b/tests/unit/auth/test_resources.py
index 9b45b68..7f0b43d 100644
--- a/tests/unit/auth/test_resources.py
+++ b/tests/unit/auth/test_resources.py
@@ -47,11 +47,11 @@ def test_create_resource(# pylint: disable=[too-many-arguments, unused-argument]
user,
tuple(client for client in clients if client.user == user)[0]))
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:
+ resource = create_resource(
+ cursor, "test_resource", resource_category, user, _group, False)
+ assert resource == expected
# Cleanup
cursor.execute(
"DELETE FROM user_roles WHERE resource_id=?",
@@ -82,8 +82,15 @@ def test_create_resource_raises_for_unauthorised_users(
tuple(client for client in clients if client.user == user)[0]))
conn, _group, _users = fxtr_users_in_group
with pytest.raises(AuthorisationError):
- assert create_resource(
- conn, "test_resource", resource_category, user, False) == expected
+ with db.cursor(conn) as cursor:
+ assert create_resource(
+ cursor,
+ "test_resource",
+ resource_category,
+ user,
+ _group,
+ False
+ ) == expected
def sort_key_resources(resource):
"""Sort-key for resources."""