diff options
Diffstat (limited to 'gn3/auth')
-rw-r--r-- | gn3/auth/authentication/users.py | 4 | ||||
-rw-r--r-- | gn3/auth/authorisation/checks.py | 2 | ||||
-rw-r--r-- | gn3/auth/authorisation/errors.py | 8 | ||||
-rw-r--r-- | gn3/auth/authorisation/groups/models.py | 60 | ||||
-rw-r--r-- | gn3/auth/authorisation/groups/views.py | 27 |
5 files changed, 93 insertions, 8 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))) |