From cd86e361c1bc1f641b5e6e05a0c18d05a8dd2ae2 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Sat, 23 Nov 2024 06:19:07 +0000 Subject: [PATCH] Add actions to group membership APIs --- h/presenters/group_membership_json.py | 41 +++++++++++++++++++++++++-- h/security/predicates.py | 28 +++++++++++------- h/traversal/group_membership.py | 2 ++ h/views/api/group_members.py | 6 ++-- 4 files changed, 61 insertions(+), 16 deletions(-) diff --git a/h/presenters/group_membership_json.py b/h/presenters/group_membership_json.py index 623ac887672..b57549f416f 100644 --- a/h/presenters/group_membership_json.py +++ b/h/presenters/group_membership_json.py @@ -1,12 +1,49 @@ +from h.models import GroupMembershipRoles +from h.security import Permission +from h.traversal.group_membership import ( + EditGroupMembershipContext, + GroupMembershipContext, +) + + class GroupMembershipJSONPresenter: - def __init__(self, membership): + def __init__(self, request, membership, authenticated_roles=None): + self.request = request self.membership = membership + self.authenticated_roles = authenticated_roles def asdict(self): - return { + membership_dict = { "authority": self.membership.group.authority, "userid": self.membership.user.userid, "username": self.membership.user.username, "display_name": self.membership.user.display_name, "roles": self.membership.roles, + "actions": [], } + + if self.request.has_permission( + Permission.Group.MEMBER_REMOVE, + GroupMembershipContext( + self.membership.group, + self.membership.user, + self.membership, + self.authenticated_roles, + ), + ): + membership_dict["actions"].append("delete") + + for role in GroupMembershipRoles: + if self.request.has_permission( + Permission.Group.MEMBER_EDIT, + EditGroupMembershipContext( + self.membership.group, + self.membership.user, + self.membership, + role, + self.authenticated_roles, + ), + ): + membership_dict["actions"].append(f"updates.roles.{role}") + + return membership_dict diff --git a/h/security/predicates.py b/h/security/predicates.py index 8e7cb05446f..ab997ce3208 100644 --- a/h/security/predicates.py +++ b/h/security/predicates.py @@ -185,17 +185,19 @@ def group_matches_authenticated_client_authority(identity, context): @requires(authenticated_user, group_found) def group_member_remove(identity, context: GroupMembershipContext): - def get_authenticated_users_membership(): - """Return the authenticated users membership in the target group.""" + def get_authenticated_users_roles(): + """Return the authenticated users roles in the target group.""" for membership in identity.user.memberships: if membership.group.id == context.group.id: - return membership + return membership.roles return None - membership = get_authenticated_users_membership() + authenticated_users_roles = ( + context.authenticated_roles or get_authenticated_users_roles() + ) - if not membership: + if not authenticated_users_roles: # You can't remove anyone from a group you're not a member of. return False @@ -205,17 +207,19 @@ def get_authenticated_users_membership(): if "owner" in context.membership.roles or "admin" in context.membership.roles: # Only owners can remove other owners. - return "owner" in membership.roles + return "owner" in authenticated_users_roles if "moderator" in context.membership.roles: # Owners and admins can remove moderators. - return "owner" in membership.roles or "admin" in membership.roles + return ( + "owner" in authenticated_users_roles or "admin" in authenticated_users_roles + ) # Owners, admins and moderators can remove plain members. return ( - "owner" in membership.roles - or "admin" in membership.roles - or "moderator" in membership.roles + "owner" in authenticated_users_roles + or "admin" in authenticated_users_roles + or "moderator" in authenticated_users_roles ) @@ -234,7 +238,9 @@ def get_authenticated_users_roles(): return None - authenticated_users_roles = get_authenticated_users_roles() + authenticated_users_roles = ( + context.authenticated_roles or get_authenticated_users_roles() + ) if not authenticated_users_roles: return False diff --git a/h/traversal/group_membership.py b/h/traversal/group_membership.py index 141a1fb311e..1bce9657f05 100644 --- a/h/traversal/group_membership.py +++ b/h/traversal/group_membership.py @@ -11,6 +11,7 @@ class GroupMembershipContext: group: Group user: User membership: GroupMembership | None + authenticated_roles: list[GroupMembershipRoles] | None = None @dataclass @@ -19,6 +20,7 @@ class EditGroupMembershipContext: user: User membership: GroupMembership new_roles: list[GroupMembershipRoles] + authenticated_roles: list[GroupMembershipRoles] | None = None def group_membership_api_factory(request) -> GroupMembershipContext: diff --git a/h/views/api/group_members.py b/h/views/api/group_members.py index fa6a1784d5d..bbcba7f0ab1 100644 --- a/h/views/api/group_members.py +++ b/h/views/api/group_members.py @@ -20,9 +20,9 @@ description="Fetch a list of all members of a group", permission=Permission.Group.READ, ) -def list_members(context: GroupContext, _request): +def list_members(context: GroupContext, request): return [ - GroupMembershipJSONPresenter(membership).asdict() + GroupMembershipJSONPresenter(request, membership).asdict() for membership in context.group.memberships ] @@ -90,4 +90,4 @@ def edit_member(context: GroupMembershipContext, request): old_roles, ) - return GroupMembershipJSONPresenter(context.membership).asdict() + return GroupMembershipJSONPresenter(request, context.membership, new_roles).asdict()