Skip to content

Commit

Permalink
Merge pull request #58 from certego/develop
Browse files Browse the repository at this point in the history
0.7.3
  • Loading branch information
mlodic authored Oct 31, 2023
2 parents 4f2483c + c5a3f02 commit 90c96c5
Show file tree
Hide file tree
Showing 7 changed files with 386 additions and 270 deletions.
5 changes: 5 additions & 0 deletions .github/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

**[Get it on PyPi](https://pypi.org/project/certego-saas/)**

## [0.7.3](https://github.com/certego/certego-saas/releases/tag/0.1.1)

**New features:**
- Admin can invite users to join the organization and revoke the invitations.

## [0.1.1](https://github.com/certego/certego-saas/releases/tag/0.1.1)

**New features:**
Expand Down
2 changes: 1 addition & 1 deletion certego_saas/apps/organization/membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,6 @@ class ExistingMembershipException(ValidationError):

class OwnerCantLeaveException(ValidationError):
default_detail = (
"Owner cannot leave the organization"
"Owner cannot leave the organization "
"but can choose to delete the organization."
)
25 changes: 21 additions & 4 deletions certego_saas/apps/organization/permissions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from rest_framework.permissions import BasePermission

from .invitation import Invitation
from .membership import Membership
from .organization import Organization


Expand All @@ -11,12 +13,17 @@ class InvitationDestroyObjectPermission(BasePermission):

message = "Invitation was previously accepted or declined so cannot be deleted."

def has_object_permission(self, request, view, obj):
def has_object_permission(self, request, view, obj: Invitation):
is_pending = obj.is_pending()
if not is_pending:
try:
Membership.objects.get(
user__username=request.user,
organization=obj.organization,
is_admin=True,
)
except Membership.DoesNotExist:
return False
is_authuser_org_owner = obj.organization.owner.pk == request.user.pk
return is_pending and is_authuser_org_owner
return is_pending


class IsObjectOwnerPermission(BasePermission):
Expand All @@ -28,6 +35,16 @@ def has_object_permission(self, request, view, obj):
return False


class IsObjectAdminPermission(BasePermission):
def has_object_permission(self, request, view, obj: Organization):
try:
return Membership.objects.get(
user__username=request.user, organization=obj, is_admin=True
)
except Membership.DoesNotExist:
return False


class IsObjectSameOrgPermission(BasePermission):
def has_object_permission(self, request, view, obj):
if isinstance(obj, Organization):
Expand Down
40 changes: 28 additions & 12 deletions certego_saas/apps/organization/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from rest_flex_fields import is_expanded
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.exceptions import NotFound, ValidationError
from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet

Expand All @@ -15,6 +15,7 @@
from .organization import Organization
from .permissions import (
InvitationDestroyObjectPermission,
IsObjectAdminPermission,
IsObjectOwnerPermission,
IsObjectSameOrgPermission,
)
Expand Down Expand Up @@ -61,7 +62,7 @@ def get_permissions(self):
if self.request.method.lower() in ["delete"]:
permissions.append(IsObjectOwnerPermission())
elif self.action in ["invite", "remove_member"]:
permissions.append(IsObjectOwnerPermission())
permissions.append(IsObjectAdminPermission())
elif self.action in ["list", "retrieve", "leave"]:
permissions.append(IsObjectSameOrgPermission())
return permissions
Expand Down Expand Up @@ -99,7 +100,7 @@ def delete(self, request):
@action(detail=False, methods=["POST"])
def invite(self, request, *args, **kwargs):
"""
Invite user to organization (accessible only to the organization owner).
Invite user to organization (accessible only to the organization admin).
``POST ~/organization/invite``.
"""
Expand All @@ -120,22 +121,33 @@ def invite(self, request, *args, **kwargs):
@action(detail=False, methods=["POST"])
def remove_member(self, request, *args, **kwargs):
"""
Remove user's membership from organization (accessible only to the organization owner).
Remove user's membership from organization (accessible only to the organization admin).
``POST ~/organization/remove_member``.
"""
username = request.data.get("username", None)
logger.info(f"remove member {username} from user {request.user}")
if not username:
username_to_remove = request.data.get("username", None)
logger.info(f"remove member {username_to_remove} from user {request.user}")
if not username_to_remove:
raise ValidationError("'username' is required.")
org = self.get_object()
try:
membership = org.members.get(user__username=username)
if membership.is_owner:
raise ValidationError("Cannot remove organization owner.")
membership_request_user = org.members.get(user__username=request.user)
membership_user_to_remove = org.members.get(
user__username=username_to_remove
)
if membership_user_to_remove.is_owner:
raise PermissionDenied(
detail="Cannot remove organization owner.", code=403
)
# only the owner can remove the admin
if (
not membership_request_user.is_owner
and membership_user_to_remove.is_admin
):
raise PermissionDenied(detail="Cannot remove another admin.", code=403)
except Membership.DoesNotExist:
raise ValidationError("No such member.")
membership.delete()
membership_user_to_remove.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

@action(detail=False, methods=["POST"])
Expand All @@ -146,7 +158,11 @@ def leave(self, request, *args, **kwargs):
``POST ~/organization/leave``.
"""
logger.info(f"leave membership org from user {request.user}")
if request.user.membership.is_owner:
try:
membership = request.user.membership
except Membership.DoesNotExist:
raise NotFound()
if membership.is_owner:
raise Membership.OwnerCantLeaveException()
request.user.membership.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
Expand Down
2 changes: 1 addition & 1 deletion certego_saas/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION = "0.7.2"
VERSION = "0.7.3"
13 changes: 12 additions & 1 deletion tests/apps/organization/test_invitation.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def test_list_200(self):
msg=msg,
)

def test_delete_204(self):
def test_delete_204_owner(self):
"""Invitation was created by user1
who is owner of organization
so only user1 can delete it too
Expand All @@ -67,6 +67,17 @@ def test_delete_204(self):
# assert for API response
self.assertEqual(204, response.status_code, msg=response)

def test_delete_204_admin(self):
user: User = User.objects.get_or_create(username="test_admin_org_1")[0]
Membership.objects.create(user=user, organization=self.org, is_admin=True)

self.client.force_authenticate(user)
# delete invitation
response = self.__delete_invitation_api(self.invitation.id)

# assert for API response
self.assertEqual(204, response.status_code, msg=response)

def test_delete_403(self):
"""invitation was created by user1
who is owner of organization,
Expand Down
Loading

0 comments on commit 90c96c5

Please sign in to comment.