diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 1b25229f3b..1cf6c89432 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to the **Prowler API** are documented in this file. ### Fixed - Search filter for findings and resources [(#8112)](https://github.com/prowler-cloud/prowler/pull/8112) +- RBAC is now applied to `GET /overviews/providers` [(#8277)](https://github.com/prowler-cloud/prowler/pull/8277) ### Security diff --git a/api/src/backend/api/tests/test_rbac.py b/api/src/backend/api/tests/test_rbac.py index 19ea256723..12271d81b9 100644 --- a/api/src/backend/api/tests/test_rbac.py +++ b/api/src/backend/api/tests/test_rbac.py @@ -1,6 +1,7 @@ from unittest.mock import ANY, Mock, patch import pytest +from conftest import TODAY from django.urls import reverse from rest_framework import status @@ -409,3 +410,87 @@ class TestLimitedVisibility: assert ( response.json()["data"]["relationships"]["providers"]["meta"]["count"] == 1 ) + + def test_overviews_providers( + self, + authenticated_client_rbac_limited, + scan_summaries_fixture, + providers_fixture, + ): + # By default, the associated provider is the one which has the overview data + response = authenticated_client_rbac_limited.get(reverse("overview-providers")) + + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["data"]) > 0 + + # Changing the provider visibility, no data should be returned + # Only the associated provider to that group is changed + new_provider = providers_fixture[1] + ProviderGroupMembership.objects.all().update(provider=new_provider) + + response = authenticated_client_rbac_limited.get(reverse("overview-providers")) + + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["data"]) == 0 + + @pytest.mark.parametrize( + "endpoint_name", + [ + "findings", + "findings_severity", + ], + ) + def test_overviews_findings( + self, + endpoint_name, + authenticated_client_rbac_limited, + scan_summaries_fixture, + providers_fixture, + ): + # By default, the associated provider is the one which has the overview data + response = authenticated_client_rbac_limited.get( + reverse(f"overview-{endpoint_name}") + ) + + assert response.status_code == status.HTTP_200_OK + values = response.json()["data"]["attributes"].values() + assert any(value > 0 for value in values) + + # Changing the provider visibility, no data should be returned + # Only the associated provider to that group is changed + new_provider = providers_fixture[1] + ProviderGroupMembership.objects.all().update(provider=new_provider) + + response = authenticated_client_rbac_limited.get( + reverse(f"overview-{endpoint_name}") + ) + + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"]["attributes"].values() + assert all(value == 0 for value in data) + + def test_overviews_services( + self, + authenticated_client_rbac_limited, + scan_summaries_fixture, + providers_fixture, + ): + # By default, the associated provider is the one which has the overview data + response = authenticated_client_rbac_limited.get( + reverse("overview-services"), {"filter[inserted_at]": TODAY} + ) + + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["data"]) > 0 + + # Changing the provider visibility, no data should be returned + # Only the associated provider to that group is changed + new_provider = providers_fixture[1] + ProviderGroupMembership.objects.all().update(provider=new_provider) + + response = authenticated_client_rbac_limited.get( + reverse("overview-services"), {"filter[inserted_at]": TODAY} + ) + + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["data"]) == 0 diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 2d59cffddd..fb0635a8dc 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -14,7 +14,13 @@ import jwt import pytest from allauth.socialaccount.models import SocialAccount, SocialApp from botocore.exceptions import ClientError, NoCredentialsError -from conftest import API_JSON_CONTENT_TYPE, TEST_PASSWORD, TEST_USER +from conftest import ( + API_JSON_CONTENT_TYPE, + TEST_PASSWORD, + TEST_USER, + TODAY, + today_after_n_days, +) from django.conf import settings from django.http import JsonResponse from django.test import RequestFactory @@ -47,14 +53,6 @@ from api.models import ( from api.rls import Tenant from api.v1.views import ComplianceOverviewViewSet, TenantFinishACSView -TODAY = str(datetime.today().date()) - - -def today_after_n_days(n_days: int) -> str: - return datetime.strftime( - datetime.today().date() + timedelta(days=n_days), "%Y-%m-%d" - ) - class TestViewSet: def test_security_headers(self, client): diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index a060c75d8f..9d38f7b11b 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -94,7 +94,6 @@ from api.filters import ( UserFilter, ) from api.models import ( - ComplianceOverview, ComplianceRequirementOverview, Finding, Integration, @@ -3469,7 +3468,7 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin): ) @method_decorator(CACHE_DECORATOR, name="list") class OverviewViewSet(BaseRLSViewSet): - queryset = ComplianceOverview.objects.all() + queryset = ScanSummary.objects.all() http_method_names = ["get"] ordering = ["-inserted_at"] # RBAC required permissions (implicit -> MANAGE_PROVIDERS enable unlimited visibility or check the visibility of @@ -3480,19 +3479,10 @@ class OverviewViewSet(BaseRLSViewSet): role = get_role(self.request.user) providers = get_providers(role) - def _get_filtered_queryset(model): - if role.unlimited_visibility: - return model.all_objects.filter(tenant_id=self.request.tenant_id) - return model.all_objects.filter( - tenant_id=self.request.tenant_id, scan__provider__in=providers - ) + if not role.unlimited_visibility: + self.allowed_providers = providers - if self.action == "providers": - return _get_filtered_queryset(Finding) - elif self.action in ("findings", "findings_severity", "services"): - return _get_filtered_queryset(ScanSummary) - else: - return super().get_queryset() + return ScanSummary.all_objects.filter(tenant_id=self.request.tenant_id) def get_serializer_class(self): if self.action == "providers": @@ -3525,18 +3515,24 @@ class OverviewViewSet(BaseRLSViewSet): @action(detail=False, methods=["get"], url_name="providers") def providers(self, request): tenant_id = self.request.tenant_id + queryset = self.get_queryset() + provider_filter = ( + {"provider__in": self.allowed_providers} + if hasattr(self, "allowed_providers") + else {} + ) latest_scan_ids = ( - Scan.all_objects.filter(tenant_id=tenant_id, state=StateChoices.COMPLETED) + Scan.all_objects.filter( + tenant_id=tenant_id, state=StateChoices.COMPLETED, **provider_filter + ) .order_by("provider_id", "-inserted_at") .distinct("provider_id") .values_list("id", flat=True) ) findings_aggregated = ( - ScanSummary.all_objects.filter( - tenant_id=tenant_id, scan_id__in=latest_scan_ids - ) + queryset.filter(scan_id__in=latest_scan_ids) .values( "scan__provider_id", provider=F("scan__provider__provider"), @@ -3572,7 +3568,7 @@ class OverviewViewSet(BaseRLSViewSet): ) return Response( - OverviewProviderSerializer(overview, many=True).data, + self.get_serializer(overview, many=True).data, status=status.HTTP_200_OK, ) @@ -3581,9 +3577,16 @@ class OverviewViewSet(BaseRLSViewSet): tenant_id = self.request.tenant_id queryset = self.get_queryset() filtered_queryset = self.filter_queryset(queryset) + provider_filter = ( + {"provider__in": self.allowed_providers} + if hasattr(self, "allowed_providers") + else {} + ) latest_scan_ids = ( - Scan.all_objects.filter(tenant_id=tenant_id, state=StateChoices.COMPLETED) + Scan.all_objects.filter( + tenant_id=tenant_id, state=StateChoices.COMPLETED, **provider_filter + ) .order_by("provider_id", "-inserted_at") .distinct("provider_id") .values_list("id", flat=True) @@ -3620,9 +3623,16 @@ class OverviewViewSet(BaseRLSViewSet): tenant_id = self.request.tenant_id queryset = self.get_queryset() filtered_queryset = self.filter_queryset(queryset) + provider_filter = ( + {"provider__in": self.allowed_providers} + if hasattr(self, "allowed_providers") + else {} + ) latest_scan_ids = ( - Scan.all_objects.filter(tenant_id=tenant_id, state=StateChoices.COMPLETED) + Scan.all_objects.filter( + tenant_id=tenant_id, state=StateChoices.COMPLETED, **provider_filter + ) .order_by("provider_id", "-inserted_at") .distinct("provider_id") .values_list("id", flat=True) @@ -3642,7 +3652,7 @@ class OverviewViewSet(BaseRLSViewSet): for item in severity_counts: severity_data[item["severity"]] = item["count"] - serializer = OverviewSeveritySerializer(severity_data) + serializer = self.get_serializer(severity_data) return Response(serializer.data, status=status.HTTP_200_OK) @action(detail=False, methods=["get"], url_name="services") @@ -3650,9 +3660,16 @@ class OverviewViewSet(BaseRLSViewSet): tenant_id = self.request.tenant_id queryset = self.get_queryset() filtered_queryset = self.filter_queryset(queryset) + provider_filter = ( + {"provider__in": self.allowed_providers} + if hasattr(self, "allowed_providers") + else {} + ) latest_scan_ids = ( - Scan.all_objects.filter(tenant_id=tenant_id, state=StateChoices.COMPLETED) + Scan.all_objects.filter( + tenant_id=tenant_id, state=StateChoices.COMPLETED, **provider_filter + ) .order_by("provider_id", "-inserted_at") .distinct("provider_id") .values_list("id", flat=True) @@ -3670,7 +3687,7 @@ class OverviewViewSet(BaseRLSViewSet): .order_by("service") ) - serializer = OverviewServiceSerializer(services_data, many=True) + serializer = self.get_serializer(services_data, many=True) return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/api/src/backend/conftest.py b/api/src/backend/conftest.py index 7f0f063f75..ebfa6178ec 100644 --- a/api/src/backend/conftest.py +++ b/api/src/backend/conftest.py @@ -46,12 +46,19 @@ from api.v1.serializers import TokenSerializer from prowler.lib.check.models import Severity from prowler.lib.outputs.finding import Status +TODAY = str(datetime.today().date()) API_JSON_CONTENT_TYPE = "application/vnd.api+json" NO_TENANT_HTTP_STATUS = status.HTTP_401_UNAUTHORIZED TEST_USER = "dev@prowler.com" TEST_PASSWORD = "testing_psswd" +def today_after_n_days(n_days: int) -> str: + return datetime.strftime( + datetime.today().date() + timedelta(days=n_days), "%Y-%m-%d" + ) + + @pytest.fixture(scope="module") def enforce_test_user_db_connection(django_db_setup, django_db_blocker): """Ensure tests use the test user for database connections."""