fix(api): scope metadata endpoints to role provider groups

This commit is contained in:
pedrooot committed 2026-10-06 14:55:24 +02:00
1 parent d38e04dd3e
commit 478ed0e577
3 files changed
+228 -11

No files matched your search

@@ -0,0 +1 @@
`GET /resources/metadata`, `GET /resources/metadata/latest`, `GET /findings/metadata` and `GET /findings/metadata/latest` now honour the provider groups of the caller role instead of aggregating every provider in the tenant
+181
View File
@@ -97,6 +97,11 @@ from rest_framework_simplejwt.token_blacklist.models import (
OutstandingToken,
)
from rest_framework_simplejwt.tokens import RefreshToken
from tasks.jobs.backfill import (
aggregate_scan_category_summaries,
aggregate_scan_resource_group_summaries,
backfill_resource_scan_summaries,
)
class TestViewSet:
@@ -6949,6 +6954,99 @@ class TestAttackPathsScanViewSet:
assert response.status_code == status.HTTP_400_BAD_REQUEST
def _client_restricted_to_provider(tenant, provider, factory):
"""Authenticated client for a fresh member whose role only sees `provider`."""
user = User.objects.create_user(
name=f"restricted-{uuid4()}",
email=f"restricted-{uuid4()}@prowler.com",
password=TEST_PASSWORD,
)
Membership.objects.create(
user=user, tenant=tenant, role=Membership.RoleChoices.MEMBER
)
role = Role.objects.create(
name=f"restricted-{uuid4()}",
tenant_id=tenant.id,
manage_users=False,
manage_account=False,
manage_billing=False,
manage_providers=False,
manage_integrations=False,
manage_scans=False,
unlimited_visibility=False,
)
UserRoleRelationship.objects.create(user=user, role=role, tenant_id=tenant.id)
provider_group = ProviderGroup.objects.create(
name=f"restricted-{uuid4()}", tenant_id=tenant.id
)
ProviderGroupMembership.objects.create(
tenant_id=tenant.id, provider_group=provider_group, provider=provider
)
RoleProviderGroupRelationship.objects.create(
tenant_id=tenant.id, role=role, provider_group=provider_group
)
return factory(user, tenant)
def _completed_scan_with_summaries(
tenant, provider, resources, *, categories, resource_group
):
"""Completed scan for `provider` with one finding over `resources`, summary tables filled."""
tenant_id = str(tenant.id)
scan = Scan.objects.create(
name=f"scan-{uuid4()}",
provider=provider,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
tenant_id=tenant_id,
)
finding = Finding.objects.create(
tenant_id=tenant_id,
uid=f"finding-{uuid4()}",
scan=scan,
delta="new",
status=Status.FAIL,
status_extended="test status",
impact=Severity.critical,
impact_extended="test impact",
severity=Severity.critical,
raw_result={"status": Status.FAIL},
check_id="scoped_check",
check_metadata={"CheckId": "scoped_check"},
categories=categories,
resource_groups=resource_group,
first_seen_at="2024-01-02T00:00:00Z",
)
finding.add_resources(resources)
backfill_resource_scan_summaries(tenant_id, str(scan.id))
aggregate_scan_category_summaries(tenant_id, str(scan.id))
aggregate_scan_resource_group_summaries(tenant_id, str(scan.id))
return scan
@pytest.fixture
def scans_per_provider_fixture(tenants_fixture, aws_provider_pair, resources_fixture):
"""One completed scan per provider; provider 2 only owns the `test` typed resource."""
tenant = tenants_fixture[0]
provider1, provider2 = aws_provider_pair
resource1, resource2, resource3 = resources_fixture
_completed_scan_with_summaries(
tenant,
provider1,
[resource1, resource2],
categories=["iam"],
resource_group="identity",
)
_completed_scan_with_summaries(
tenant,
provider2,
[resource3],
categories=["data-protection"],
resource_group="data",
)
return tenant, provider2
@pytest.mark.django_db
class TestResourceViewSet:
def test_resources_list_none(self, authenticated_client):
@@ -7320,6 +7418,42 @@ class TestResourceViewSet:
assert attributes["types"] == [latest_scan_resource.type]
assert "groups" in attributes
def test_resources_metadata_restricted_to_provider_group(
self, authenticated_client_for_tenant_factory, scans_per_provider_fixture
):
tenant, provider2 = scans_per_provider_fixture
client = _client_restricted_to_provider(
tenant, provider2, authenticated_client_for_tenant_factory
)
response = client.get(
reverse("resource-metadata"), {"filter[updated_at]": TODAY}
)
assert response.status_code == status.HTTP_200_OK
attributes = response.json()["data"]["attributes"]
assert set(attributes["services"]) == {"ec2"}
assert set(attributes["regions"]) == {"us-east-1"}
assert set(attributes["types"]) == {"test"}
assert set(attributes["groups"]) == {"compute"}
def test_resources_metadata_latest_restricted_to_provider_group(
self, authenticated_client_for_tenant_factory, scans_per_provider_fixture
):
tenant, provider2 = scans_per_provider_fixture
client = _client_restricted_to_provider(
tenant, provider2, authenticated_client_for_tenant_factory
)
response = client.get(reverse("resource-metadata_latest"))
assert response.status_code == status.HTTP_200_OK
attributes = response.json()["data"]["attributes"]
assert set(attributes["services"]) == {"ec2"}
assert set(attributes["regions"]) == {"us-east-1"}
assert set(attributes["types"]) == {"test"}
assert set(attributes["groups"]) == {"compute"}
def test_resources_latest_filter_by_provider_id(
self, authenticated_client, latest_scan_resource
):
@@ -8952,6 +9086,53 @@ class TestFindingViewSet:
assert "groups" in attributes
assert "ai_ml" in attributes["groups"]
@patch("api.v1.views.backfill_scan_resource_summaries_task.apply_async")
def test_findings_metadata_restricted_to_provider_group(
self,
mock_backfill,
authenticated_client_for_tenant_factory,
scans_per_provider_fixture,
):
tenant, provider2 = scans_per_provider_fixture
client = _client_restricted_to_provider(
tenant, provider2, authenticated_client_for_tenant_factory
)
response = client.get(
reverse("finding-metadata"), {"filter[inserted_at]": TODAY}
)
assert response.status_code == status.HTTP_200_OK
mock_backfill.assert_not_called()
attributes = response.json()["data"]["attributes"]
assert set(attributes["services"]) == {"ec2"}
assert set(attributes["regions"]) == {"us-east-1"}
assert set(attributes["resource_types"]) == {"test"}
assert set(attributes["categories"]) == {"data-protection"}
@patch("api.v1.views.backfill_scan_resource_summaries_task.apply_async")
def test_findings_metadata_latest_restricted_to_provider_group(
self,
mock_backfill,
authenticated_client_for_tenant_factory,
scans_per_provider_fixture,
):
tenant, provider2 = scans_per_provider_fixture
client = _client_restricted_to_provider(
tenant, provider2, authenticated_client_for_tenant_factory
)
response = client.get(reverse("finding-metadata_latest"))
assert response.status_code == status.HTTP_200_OK
mock_backfill.assert_not_called()
attributes = response.json()["data"]["attributes"]
assert set(attributes["services"]) == {"ec2"}
assert set(attributes["regions"]) == {"us-east-1"}
assert set(attributes["resource_types"]) == {"test"}
assert set(attributes["categories"]) == {"data-protection"}
assert set(attributes["groups"]) == {"data"}
def test_findings_filter_by_category(
self, authenticated_client, findings_with_categories
):
+46 -11
View File
@@ -351,6 +351,15 @@ from tasks.tasks import (
logger = logging.getLogger(BackendLogger.API)
def _scans_visible_to(role, tenant_id):
"""Tenant scans, narrowed to the role's provider groups unless it has unlimited visibility."""
scans = Scan.all_objects.filter(tenant_id=tenant_id)
if role.unlimited_visibility:
return scans
return scans.filter(provider__in=get_providers(role))
CACHE_DECORATOR = cache_control(
max_age=django_settings.CACHE_MAX_AGE,
stale_while_revalidate=django_settings.CACHE_STALE_WHILE_REVALIDATE,
@@ -3544,8 +3553,15 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet):
tenant_id = request.tenant_id
query_params = request.query_params
user_roles = get_role(request.user, tenant_id)
queryset = ResourceScanSummary.objects.filter(tenant_id=tenant_id)
if not user_roles.unlimited_visibility:
queryset = queryset.filter(
scan_id__in=_scans_visible_to(user_roles, tenant_id).values_list(
"id", flat=True
)
)
if scans := query_params.get("filter[scan__in]") or query_params.get(
"filter[scan]"
@@ -3623,7 +3639,10 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet):
all_groups = Resource.objects.filter(
tenant_id=tenant_id,
groups__isnull=False,
).values_list("groups", flat=True)
)
if not user_roles.unlimited_visibility:
all_groups = all_groups.filter(provider__in=get_providers(user_roles))
all_groups = all_groups.values_list("groups", flat=True)
groups = sorted(
{g for groups_list in all_groups if groups_list for g in groups_list}
)
@@ -3648,9 +3667,10 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet):
def metadata_latest(self, request):
tenant_id = request.tenant_id
query_params = request.query_params
user_roles = get_role(request.user, tenant_id)
latest_scans_queryset = Scan.all_objects.filter(
tenant_id=tenant_id
latest_scans_queryset = _scans_visible_to(
user_roles, tenant_id
).latest_per_provider()
queryset = ResourceScanSummary.objects.filter(
@@ -3691,7 +3711,10 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet):
all_groups = Resource.objects.filter(
tenant_id=tenant_id,
groups__isnull=False,
).values_list("groups", flat=True)
)
if not user_roles.unlimited_visibility:
all_groups = all_groups.filter(provider__in=get_providers(user_roles))
all_groups = all_groups.values_list("groups", flat=True)
groups = sorted(
{g for groups_list in all_groups if groups_list for g in groups_list}
)
@@ -4102,8 +4125,14 @@ class FindingViewSet(PaginateByPkMixin, BaseRLSViewSet):
tenant_id = request.tenant_id
query_params = request.query_params
user_roles = get_role(request.user, tenant_id)
visible_scan_ids = _scans_visible_to(user_roles, tenant_id).values_list(
"id", flat=True
)
queryset = ResourceScanSummary.objects.filter(tenant_id=tenant_id)
if not user_roles.unlimited_visibility:
queryset = queryset.filter(scan_id__in=visible_scan_ids)
scan_based_filters = {}
category_scan_filters = {} # Filters for ScanCategorySummary
@@ -4161,7 +4190,10 @@ class FindingViewSet(PaginateByPkMixin, BaseRLSViewSet):
if not queryset.exists():
raw_scans_ids = Scan.objects.filter(
tenant_id=tenant_id, **scan_based_filters
).values_list("id", "unique_resource_count")
)
if not user_roles.unlimited_visibility:
raw_scans_ids = raw_scans_ids.filter(id__in=visible_scan_ids)
raw_scans_ids = raw_scans_ids.values_list("id", "unique_resource_count")
scan_ids = [
scan_id for scan_id, count in raw_scans_ids if count and count > 0
]
@@ -4203,11 +4235,13 @@ class FindingViewSet(PaginateByPkMixin, BaseRLSViewSet):
)
# Get categories from ScanCategorySummary using same scan filters
category_summaries = ScanCategorySummary.objects.filter(
tenant_id=tenant_id, **category_scan_filters
)
if not user_roles.unlimited_visibility:
category_summaries = category_summaries.filter(scan_id__in=visible_scan_ids)
categories = list(
ScanCategorySummary.objects.filter(
tenant_id=tenant_id, **category_scan_filters
)
.values_list("category", flat=True)
category_summaries.values_list("category", flat=True)
.distinct()
.order_by("category")
)
@@ -4262,9 +4296,10 @@ class FindingViewSet(PaginateByPkMixin, BaseRLSViewSet):
def metadata_latest(self, request):
tenant_id = request.tenant_id
query_params = request.query_params
user_roles = get_role(request.user, tenant_id)
latest_scans_queryset = Scan.all_objects.filter(
tenant_id=tenant_id
latest_scans_queryset = _scans_visible_to(
user_roles, tenant_id
).latest_per_provider()
raw_latest_scans_ids = list(
latest_scans_queryset.values_list("id", "unique_resource_count")