about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFrederick Muriuki Muriithi2023-03-06 14:57:53 +0300
committerFrederick Muriuki Muriithi2023-03-06 14:57:53 +0300
commit98e93be1b8e5353656e18f1452026db6f2902e6c (patch)
tree2547ab9284e1a1718b35faf92d8aa68e9d42b283
parent4fc72af7e851f12a9f4edc98b0a55c66c9bf1b13 (diff)
downloadgenenetwork3-98e93be1b8e5353656e18f1452026db6f2902e6c.tar.gz
auth: resources: Enable assigning a user roles on resources
-rw-r--r--gn3/auth/authentication/oauth2/grants/password_grant.py10
-rw-r--r--gn3/auth/authentication/users.py7
-rw-r--r--gn3/auth/authorisation/resources/models.py30
-rw-r--r--gn3/auth/authorisation/resources/views.py38
-rw-r--r--gn3/auth/authorisation/users/models.py19
-rw-r--r--gn3/auth/authorisation/users/views.py9
-rw-r--r--main.py3
-rw-r--r--migrations/auth/20230306_01_pRfxl-add-system-user-list-privilege.py26
-rw-r--r--migrations/auth/20230306_02_7GnRY-add-system-user-list-privilege-to-system-administrator-and-group-leader-roles.py42
9 files changed, 169 insertions, 15 deletions
diff --git a/gn3/auth/authentication/oauth2/grants/password_grant.py b/gn3/auth/authentication/oauth2/grants/password_grant.py
index 3ec7384..3233877 100644
--- a/gn3/auth/authentication/oauth2/grants/password_grant.py
+++ b/gn3/auth/authentication/oauth2/grants/password_grant.py
@@ -6,6 +6,8 @@ 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"]
@@ -13,6 +15,8 @@ class PasswordGrant(grants.ResourceOwnerPasswordCredentialsGrant):
     def authenticate_user(self, username, password):
         "Authenticate the user with their username and password."
         with db.connection(app.config["AUTH_DB"]) as conn:
-            return user_by_email(conn, username).maybe(
-                None,
-                lambda user: valid_login(conn, user, password) and user)
+            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/users.py b/gn3/auth/authentication/users.py
index ce01805..e65938e 100644
--- a/gn3/auth/authentication/users.py
+++ b/gn3/auth/authentication/users.py
@@ -6,6 +6,7 @@ import bcrypt
 from pymonad.maybe import Just, Maybe, Nothing
 
 from gn3.auth import db
+from gn3.auth.authorisation.errors import NotFoundError
 
 class User(NamedTuple):
     """Class representing a user."""
@@ -25,16 +26,16 @@ DUMMY_USER = User(user_id=UUID("a391cf60-e8b7-4294-bd22-ddbbda4b3530"),
                   email="gn3@dummy.user",
                   name="Dummy user to use as placeholder")
 
-def user_by_email(conn: db.DbConnection, email: str) -> Maybe:
+def user_by_email(conn: db.DbConnection, email: str) -> User:
     """Retrieve user from database by their email address"""
     with db.cursor(conn) as cursor:
         cursor.execute("SELECT * FROM users WHERE email=?", (email,))
         row = cursor.fetchone()
 
     if row:
-        return Just(User(UUID(row["user_id"]), row["email"], row["name"]))
+        return User(UUID(row["user_id"]), row["email"], row["name"])
 
-    return Nothing
+    raise NotFoundError(f"Could not find user with email {email}")
 
 def user_by_id(conn: db.DbConnection, user_id: UUID) -> Maybe:
     """Retrieve user from database by their user id"""
diff --git a/gn3/auth/authorisation/resources/models.py b/gn3/auth/authorisation/resources/models.py
index be1bc17..0a5b1ec 100644
--- a/gn3/auth/authorisation/resources/models.py
+++ b/gn3/auth/authorisation/resources/models.py
@@ -14,7 +14,8 @@ from .checks import authorised_for
 
 from ..checks import authorised_p
 from ..errors import NotFoundError, AuthorisationError
-from ..groups.models import Group, user_group, group_by_id, is_group_leader
+from ..groups.models import (
+    Group, GroupRole, user_group, group_by_id, is_group_leader)
 
 class MissingGroupError(AuthorisationError):
     """Raised for any resource operation without a group."""
@@ -477,3 +478,30 @@ def attach_resources_data(
                 cursor, rscs)
              for category, rscs in organised.items())
             for resource in categories)
+
+@authorised_p(
+    ("group:user:assign-role",),
+    "You cannot assign roles to users for this group.",
+    oauth2_scope="profile group role resource")
+def assign_resource_user(
+        conn: db.DbConnection, resource: Resource, user: User,
+        role: GroupRole) -> dict:
+    """Assign `role` to `user` for the specific `resource`."""
+    with db.cursor(conn) as cursor:
+        cursor.execute(
+            "INSERT INTO "
+            "group_user_roles_on_resources(group_id, user_id, role_id, "
+            "resource_id) "
+            "VALUES (?, ?, ?, ?) "
+            "ON CONFLICT (group_id, user_id, role_id, resource_id) "
+            "DO NOTHING",
+            (str(resource.group.group_id), str(user.user_id),
+             str(role.role.role_id), str(resource.resource_id)))
+        return {
+            "resource": dictify(resource),
+            "user": dictify(user),
+            "role": dictify(role),
+            "description": (
+                f"The user '{user.name}'({user.email}) was assigned the "
+                f"'{role.role.role_name}' role on resource with ID "
+                f"'{resource.resource_id}'.")}
diff --git a/gn3/auth/authorisation/resources/views.py b/gn3/auth/authorisation/resources/views.py
index e1386ab..6d4098a 100644
--- a/gn3/auth/authorisation/resources/views.py
+++ b/gn3/auth/authorisation/resources/views.py
@@ -9,17 +9,17 @@ from gn3.auth.db_utils import with_db_connection
 
 from .checks import authorised_for
 from .models import (
-    resource_by_id, resource_categories, link_data_to_resource,
-    resource_category_by_id, unlink_data_from_resource,
+    resource_by_id, resource_categories, assign_resource_user,
+    link_data_to_resource, resource_category_by_id, unlink_data_from_resource,
     create_resource as _create_resource)
 
 from ..roles import Role
-from ..groups.models import Group, GroupRole
 from ..errors import InvalidData, AuthorisationError
+from ..groups.models import Group, GroupRole, group_role_by_id
 
 from ... import db
 from ...dictify import dictify
-from ...authentication.users import User
+from ...authentication.users import User, user_by_email
 from ...authentication.oauth2.resource_server import require_oauth
 
 resources = Blueprint("resources", __name__)
@@ -101,7 +101,7 @@ def unlink_data():
     except AssertionError as aserr:
         raise InvalidData(aserr.args[0]) from aserr
 
-@resources.route("<uuid:resource_id>/users", methods=["GET"])
+@resources.route("<uuid:resource_id>/user/list", methods=["GET"])
 @require_oauth("profile group resource")
 def resource_users(resource_id: uuid.UUID):
     """Retrieve all users with access to the given resource."""
@@ -115,8 +115,8 @@ def resource_users(resource_id: uuid.UUID):
                 with db.cursor(conn) as cursor:
                     def __organise_users_n_roles__(users_n_roles, row):
                         user_id = uuid.UUID(row["user_id"])
-                        user = users_n_roles.get(
-                            user_id, User(user_id, row["email"], row["name"]))
+                        user = users_n_roles.get(user_id, {}).get(
+                            "user", User(user_id, row["email"], row["name"]))
                         role = GroupRole(
                             uuid.UUID(row["group_role_id"]),
                             resource.group,
@@ -157,3 +157,27 @@ def resource_users(resource_id: uuid.UUID):
                 user_row for user_id, user_row
                 in with_db_connection(__the_users__).items()))
         return jsonify(tuple(results))
+
+@resources.route("<uuid:resource_id>/user/assign", methods=["POST"])
+@require_oauth("profile group resource role")
+def assign_role_to_user(resource_id: uuid.UUID) -> Response:
+    """Assign a role on the specified resource to a user."""
+    with require_oauth.acquire("profile group resource role") as the_token:
+        try:
+            form = request.form
+            group_role_id = form.get("group_role_id", "")
+            user_email = form.get("user_email", "")
+            assert bool(group_role_id), "The role must be provided."
+            assert bool(user_email), "The user email must be provided."
+
+            def __assign__(conn: db.DbConnection) -> dict:
+                resource = resource_by_id(conn, the_token.user, resource_id)
+                user = user_by_email(conn, user_email)
+                return assign_resource_user(
+                    conn, resource, user,
+                    group_role_by_id(conn, resource.group,
+                                     uuid.UUID(group_role_id)))
+        except AssertionError as aserr:
+            raise AuthorisationError(aserr.args[0]) from aserr
+
+        return jsonify(with_db_connection(__assign__))
diff --git a/gn3/auth/authorisation/users/models.py b/gn3/auth/authorisation/users/models.py
new file mode 100644
index 0000000..844a8a9
--- /dev/null
+++ b/gn3/auth/authorisation/users/models.py
@@ -0,0 +1,19 @@
+"""Functions for acting on users."""
+import uuid
+
+from gn3.auth import db
+from gn3.auth.authorisation.checks import authorised_p
+
+from gn3.auth.authentication.users import User
+
+@authorised_p(
+    ("system:user:list",),
+    "You do not have the appropriate privileges to list users.",
+    oauth2_scope="profile user")
+def list_users(conn: db.DbConnection) -> tuple[User, ...]:
+    """List out all users."""
+    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())
diff --git a/gn3/auth/authorisation/users/views.py b/gn3/auth/authorisation/users/views.py
index 2219440..5015cac 100644
--- a/gn3/auth/authorisation/users/views.py
+++ b/gn3/auth/authorisation/users/views.py
@@ -11,6 +11,7 @@ from gn3.auth import db
 from gn3.auth.dictify import dictify
 from gn3.auth.db_utils import with_db_connection
 
+from ..users.models import list_users
 from ..groups.models import user_group as _user_group
 from ..resources.models import user_resources as _user_resources
 from ..roles.models import assign_default_roles, user_roles as _user_roles
@@ -158,3 +159,11 @@ def user_join_request_exists():
     with require_oauth.acquire("profile group") as the_token:
         return jsonify(with_db_connection(partial(
             __request_exists__, user=the_token.user)))
+
+@users.route("/list", methods=["GET"])
+@require_oauth("profile user")
+def list_all_users() -> Response:
+    """List all the users."""
+    with require_oauth.acquire("profile group") as _the_token:
+        return jsonify(tuple(
+            dictify(user) for user in with_db_connection(list_users)))
diff --git a/main.py b/main.py
index 2a47dbb..6dadac2 100644
--- a/main.py
+++ b/main.py
@@ -81,7 +81,8 @@ def init_dev_clients():
             "default_redirect_uri": "http://localhost:5033/oauth2/code",
             "redirect_uris": ["http://localhost:5033/oauth2/code"],
             "response_type": "token", # choices: ["code", "token"]
-            "scope": ["profile", "group", "role", "resource", "register-client"]
+            "scope": ["profile", "group", "role", "resource", "register-client",
+                      "user"]
         }),
         "user_id": "0ad1917c-57da-46dc-b79e-c81c91e5b928"},)
 
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
new file mode 100644
index 0000000..0393cd3
--- /dev/null
+++ b/migrations/auth/20230306_01_pRfxl-add-system-user-list-privilege.py
@@ -0,0 +1,26 @@
+"""
+Add system:user:list privilege
+"""
+import contextlib
+
+from yoyo import step
+
+__depends__ = {'20230216_02_0ZHSl-make-dataset-id-and-trait-id-foreign-keys-in-tables'}
+
+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
new file mode 100644
index 0000000..4cfd068
--- /dev/null
+++ b/migrations/auth/20230306_02_7GnRY-add-system-user-list-privilege-to-system-administrator-and-group-leader-roles.py
@@ -0,0 +1,42 @@
+"""
+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(role_ids(cursor)))
+
+steps = [
+    step(add_privilege_to_roles, del_privilege_from_roles)
+]