about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFrederick Muriuki Muriithi2023-02-08 17:24:35 +0300
committerFrederick Muriuki Muriithi2023-02-08 17:44:50 +0300
commit9b94ec57c504acb6ef815d134144c4c357c71f17 (patch)
tree3bf1647953956e6abada8344fabeb87ef526e731
parent87586bd5270140bf52d39d3dc61c754dd13d4391 (diff)
downloadgenenetwork3-9b94ec57c504acb6ef815d134144c4c357c71f17.tar.gz
auth: groups: handle join requests.
-rw-r--r--gn3/auth/authentication/users.py4
-rw-r--r--gn3/auth/authorisation/checks.py2
-rw-r--r--gn3/auth/authorisation/errors.py8
-rw-r--r--gn3/auth/authorisation/groups/models.py60
-rw-r--r--gn3/auth/authorisation/groups/views.py27
-rw-r--r--migrations/auth/20230207_01_r0bkZ-create-group-join-requests-table.py (renamed from migrations/auth/20230207_01_r0bkZ-create-group-requests-table.py)9
-rw-r--r--tests/unit/auth/test_migrations_create_tables.py3
7 files changed, 100 insertions, 13 deletions
diff --git a/gn3/auth/authentication/users.py b/gn3/auth/authentication/users.py
index b2d8d53..ce01805 100644
--- a/gn3/auth/authentication/users.py
+++ b/gn3/auth/authentication/users.py
@@ -21,6 +21,10 @@ class User(NamedTuple):
         """Return a dict representation of `User` objects."""
         return {"user_id": self.user_id, "email": self.email, "name": self.name}
 
+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:
     """Retrieve user from database by their email address"""
     with db.cursor(conn) as cursor:
diff --git a/gn3/auth/authorisation/checks.py b/gn3/auth/authorisation/checks.py
index 36ab5fa..9b0af5f 100644
--- a/gn3/auth/authorisation/checks.py
+++ b/gn3/auth/authorisation/checks.py
@@ -12,7 +12,7 @@ from .errors import AuthorisationError
 from ..authentication.oauth2.resource_server import require_oauth
 
 def authorised_p(
-        privileges: tuple[str],
+        privileges: tuple[str, ...],
         error_description: str = (
             "You lack authorisation to perform requested action"),
         oauth2_scope = "profile"):
diff --git a/gn3/auth/authorisation/errors.py b/gn3/auth/authorisation/errors.py
index e1fb1a0..99ee55d 100644
--- a/gn3/auth/authorisation/errors.py
+++ b/gn3/auth/authorisation/errors.py
@@ -6,7 +6,7 @@ class AuthorisationError(Exception):
 
     All exceptions in this package should inherit from this class.
     """
-    error_code: int = 500
+    error_code: int = 400
 
 class UserRegistrationError(AuthorisationError):
     """Raised whenever a user registration fails"""
@@ -14,3 +14,9 @@ class UserRegistrationError(AuthorisationError):
 class NotFoundError(AuthorisationError):
     """Raised whenever we try fetching (a/an) object(s) that do(es) not exist."""
     error_code: int = 404
+
+class InconsistencyError(AuthorisationError):
+    """
+    Exception raised due to data inconsistencies
+    """
+    error_code: int = 500
diff --git a/gn3/auth/authorisation/groups/models.py b/gn3/auth/authorisation/groups/models.py
index 49b5066..f78aedd 100644
--- a/gn3/auth/authorisation/groups/models.py
+++ b/gn3/auth/authorisation/groups/models.py
@@ -8,11 +8,11 @@ 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
+from gn3.auth.authentication.users import User, user_by_id, DUMMY_USER
 
 from ..checks import authorised_p
 from ..privileges import Privilege
-from ..errors import NotFoundError, AuthorisationError
+from ..errors import NotFoundError, AuthorisationError, InconsistencyError
 from ..roles.models import (
     Role, create_role, revoke_user_role_by_name, assign_user_role_by_name)
 
@@ -29,6 +29,13 @@ class Group(NamedTuple):
             "group_metadata": self.group_metadata
         }
 
+DUMMY_GROUP = Group(
+    group_id=UUID("77cee65b-fe29-4383-ae41-3cb3b480cc70"),
+    group_name="GN3_DUMMY_GROUP",
+    group_metadata={
+        "group-description": "This is a dummy group to use as a placeholder"
+    })
+
 class GroupRole(NamedTuple):
     """Class representing a role tied/belonging to a group."""
     group_role_id: UUID
@@ -242,3 +249,52 @@ def group_by_id(conn: db.DbConnection, group_id: UUID) -> Group:
                 json.loads(row["group_metadata"]))
 
     raise NotFoundError(f"Could not find group with ID '{group_id}'.")
+
+@authorised_p(("system:group:view-group", "system:group:edit-group"),
+              error_description=("You do not have the appropriate authorisation"
+                                 " to act upon the join requests."),
+              oauth2_scope="profile group")
+def join_requests(conn: db.DbConnection, user: User):
+    """List all the join requests for the user's group."""
+    with db.cursor(conn) as cursor:
+        group = user_group(cursor, user).maybe(DUMMY_GROUP, lambda grp: grp)# type: ignore[misc]
+        if group != DUMMY_GROUP and is_group_leader(cursor, user, group):
+            cursor.execute(
+                "SELECT gjr.*, u.email, u.name FROM group_join_requests AS gjr "
+                "INNER JOIN users AS u ON gjr.requester_id=u.user_id "
+                "WHERE gjr.group_id=? AND gjr.status='PENDING'",
+                (str(group.group_id),))
+            return tuple(dict(row)for row in cursor.fetchall())
+
+    raise AuthorisationError(
+        "You do not have the appropriate authorisation to access the "
+        "group's join requests.")
+
+@authorised_p(("system:group:view-group", "system:group:edit-group"),
+              error_description=("You do not have the appropriate authorisation"
+                                 " to act upon the join requests."),
+              oauth2_scope="profile group")
+def accept_join_request(conn: db.DbConnection, request_id: UUID, user: User):
+    """Accept a join request."""
+    with db.cursor(conn) as cursor:
+        group = user_group(cursor, user).maybe(DUMMY_GROUP, lambda grp: grp) # type: ignore[misc]
+        cursor.execute("SELECT * FROM group_join_requests WHERE request_id=?",
+                       (str(request_id),))
+        row = cursor.fetchone()
+        if row:
+            if group.group_id == UUID(row["group_id"]):
+                the_user = user_by_id(conn, UUID(row["requester_id"])).maybe(# type: ignore[misc]
+                    DUMMY_USER, lambda usr: usr)
+                if the_user == DUMMY_USER:
+                    raise InconsistencyError(
+                        "Could not find user associated with join request.")
+                add_user_to_group(cursor, group, the_user)
+                revoke_user_role_by_name(cursor, the_user, "group-creator")
+                cursor.execute(
+                    "UPDATE group_join_requests SET status='ACCEPTED' "
+                    "WHERE request_id=?",
+                    (str(request_id),))
+                return {"request_id": request_id, "status": "ACCEPTED"}
+            raise AuthorisationError(
+                "You cannot act on other groups join requests")
+        raise NotFoundError(f"Could not find request with ID '{request_id}'")
diff --git a/gn3/auth/authorisation/groups/views.py b/gn3/auth/authorisation/groups/views.py
index f6675ab..f12c75c 100644
--- a/gn3/auth/authorisation/groups/views.py
+++ b/gn3/auth/authorisation/groups/views.py
@@ -10,7 +10,8 @@ from gn3.auth.dictify import dictify
 from gn3.auth.db_utils import with_db_connection
 
 from .models import (
-    user_group, all_groups, GroupCreationError, group_users as _group_users,
+    user_group, all_groups, join_requests, accept_join_request,
+    GroupCreationError, group_users as _group_users,
     create_group as _create_group)
 
 from ..errors import AuthorisationError
@@ -76,14 +77,14 @@ def request_to_join(group_id: uuid.UUID) -> Response:
                 raise error
             request_id = uuid.uuid4()
             cursor.execute(
-                "INSERT INTO group_requests VALUES "
-                "(:request_id, :group_id, :user_id, :ts, :type, :msg)",
+                "INSERT INTO group_join_requests VALUES "
+                "(:request_id, :group_id, :user_id, :ts, :status, :msg)",
                 {
                     "request_id": str(request_id),
                     "group_id": str(group_id),
                     "user_id": str(user.user_id),
                     "ts": datetime.datetime.now().timestamp(),
-                    "type": "JOIN",
+                    "status": "PENDING",
                     "msg": message
                 })
             return {
@@ -97,3 +98,21 @@ def request_to_join(group_id: uuid.UUID) -> Response:
             __request__, user=the_token.user, group_id=group_id, message=form.get(
                 "message", "I hereby request that you add me to your group.")))
         return jsonify(results)
+
+@groups.route("/requests/join/list", methods=["GET"])
+@require_oauth("profile group")
+def list_join_requests() -> Response:
+    """List the pending join requests."""
+    with require_oauth.acquire("profile group") as the_token:
+        return jsonify(with_db_connection(partial(
+            join_requests, user=the_token.user)))
+
+@groups.route("/requests/join/accept", methods=["POST"])
+@require_oauth("profile group")
+def accept_join_requests() -> Response:
+    """Accept a join request."""
+    with require_oauth.acquire("profile group") as the_token:
+        form = request.form
+        request_id = uuid.UUID(form.get("request_id"))
+        return jsonify(with_db_connection(partial(
+            accept_join_request, request_id=request_id, user=the_token.user)))
diff --git a/migrations/auth/20230207_01_r0bkZ-create-group-requests-table.py b/migrations/auth/20230207_01_r0bkZ-create-group-join-requests-table.py
index d2cf786..6b06a64 100644
--- a/migrations/auth/20230207_01_r0bkZ-create-group-requests-table.py
+++ b/migrations/auth/20230207_01_r0bkZ-create-group-join-requests-table.py
@@ -9,19 +9,20 @@ __depends__ = {'20230116_01_KwuJ3-rework-privileges-schema'}
 steps = [
     step(
         """
-        CREATE TABLE IF NOT EXISTS group_requests(
+        CREATE TABLE IF NOT EXISTS group_join_requests(
             request_id TEXT NOT NULL,
             group_id TEXT NOT NULL,
             requester_id TEXT NOT NULL,
-            request_type 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
+            ON UPDATE CASCADE ON DELETE CASCADE,
+            CHECK (status IN ('PENDING', 'ACCEPTED', 'REJECTED'))
         ) WITHOUT ROWID
         """,
-        "DROP TABLE IF EXISTS group_requests")
+        "DROP TABLE IF EXISTS group_join_requests")
 ]
diff --git a/tests/unit/auth/test_migrations_create_tables.py b/tests/unit/auth/test_migrations_create_tables.py
index 55d51aa..98022eb 100644
--- a/tests/unit/auth/test_migrations_create_tables.py
+++ b/tests/unit/auth/test_migrations_create_tables.py
@@ -31,7 +31,8 @@ migrations_and_tables = (
     ("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-requests-table.py", "group_requests"))
+    ("20230207_01_r0bkZ-create-group-join-requests-table.py",
+     "group_join_requests"))
 
 @pytest.mark.unit_test
 @pytest.mark.parametrize("migration_file,the_table", migrations_and_tables)