Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: prevent unauthorised remove-users access #3791

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion api/organisations/permissions/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Model
from django.views import View
from rest_framework.exceptions import PermissionDenied
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.permissions import BasePermission, IsAuthenticated
from rest_framework.request import Request

Expand Down Expand Up @@ -88,6 +88,17 @@ class OrganisationPermission(BasePermission):
def has_permission(self, request, view):
if view.action == "create" and settings.RESTRICT_ORG_CREATE_TO_SUPERUSERS:
return request.user.is_superuser

organisation_id = view.kwargs.get("pk")
if organisation_id and not organisation_id.isnumeric():
raise ValidationError("Invalid organisation ID")

if view.action == "remove_users":
return request.user.is_organisation_admin(int(organisation_id))

if organisation_id:
return request.user.belongs_to(int(organisation_id))

return True

def has_object_permission(self, request, view, obj):
Expand Down
63 changes: 63 additions & 0 deletions api/tests/unit/organisations/test_unit_organisations_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1797,3 +1797,66 @@ def test_doesnt_retrieve_stale_api_usage_notifications(
# Then
assert response.status_code == status.HTTP_200_OK
assert len(response.data["results"]) == 0

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a test for non-numeric org id.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


def test_non_organisation_user_cannot_remove_user_from_organisation(
staff_user: FFAdminUser, organisation: Organisation, api_client: APIClient
) -> None:
# Given
another_organisation = Organisation.objects.create(name="another organisation")
another_user = FFAdminUser.objects.create(email="[email protected]")
another_user.add_organisation(another_organisation)
api_client.force_authenticate(another_user)

url = reverse(
"api-v1:organisations:organisation-remove-users", args=[organisation.id]
)

data = [{"id": staff_user.id}]

# When
response = api_client.post(
url, data=json.dumps(data), content_type="application/json"
)

# Then
assert response.status_code == status.HTTP_403_FORBIDDEN


def test_non_admin_user_cannot_remove_user_from_organisation(
staff_user: FFAdminUser,
organisation: Organisation,
staff_client: APIClient,
admin_user: FFAdminUser,
) -> None:
# Given
url = reverse(
"api-v1:organisations:organisation-remove-users", args=[organisation.id]
)

data = [{"id": admin_user.id}]

# When
response = staff_client.post(
url, data=json.dumps(data), content_type="application/json"
)

# Then
assert response.status_code == status.HTTP_403_FORBIDDEN


def test_validation_error_if_non_numeric_organisation_id(
staff_client: APIClient,
) -> None:
# Given
url = reverse("api-v1:organisations:organisation-remove-users", args=["foo"])

data = []

# When
response = staff_client.post(
url, data=json.dumps(data), content_type="application/json"
)

# Then
assert response.status_code == status.HTTP_400_BAD_REQUEST