diff --git a/poetry.lock b/poetry.lock index 7e61747cf5..94d3af0d4b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1398,13 +1398,13 @@ testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"] [[package]] name = "django-filter" -version = "24.2" +version = "24.3" description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." optional = false python-versions = ">=3.8" files = [ - {file = "django-filter-24.2.tar.gz", hash = "sha256:48e5fc1da3ccd6ca0d5f9bb550973518ce977a4edde9d2a8a154a7f4f0b9f96e"}, - {file = "django_filter-24.2-py3-none-any.whl", hash = "sha256:df2ee9857e18d38bed203c8745f62a803fa0f31688c9fe6f8e868120b1848e48"}, + {file = "django_filter-24.3-py3-none-any.whl", hash = "sha256:c4852822928ce17fb699bcfccd644b3574f1a2d80aeb2b4ff4f16b02dd49dc64"}, + {file = "django_filter-24.3.tar.gz", hash = "sha256:d8ccaf6732afd21ca0542f6733b11591030fa98669f8d15599b358e24a2cd9c3"}, ] [package.dependencies] @@ -4882,4 +4882,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "a6315563a2c8cdcb3c44644b70661fee0b28ffbd20ddb5f6989eef3f9cf5eaf8" +content-hash = "6053bca227f7d7b27b4ca023fb41afc336b919331196c0bef1daa81636948c61" diff --git a/pyproject.toml b/pyproject.toml index d00cfec40a..fceb8e1f02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ django = "5.1.1" django-celery-results = "^2.5.1" django-cors-headers = "4.4.0" django-environ = "0.11.2" -django-filter = "24.2" +django-filter = "24.3" django-guid = "3.5.0" django-postgres-extra = "^2.0.8" djangorestframework = "3.15.2" diff --git a/src/backend/api/filters.py b/src/backend/api/filters.py index 43b215e13b..e9ef112d2e 100644 --- a/src/backend/api/filters.py +++ b/src/backend/api/filters.py @@ -4,16 +4,23 @@ from uuid import UUID from django.conf import settings from django.db.models import Q from django_filters.rest_framework import ( + BaseInFilter, FilterSet, BooleanFilter, CharFilter, UUIDFilter, DateFilter, + ChoiceFilter, ) from rest_framework_json_api.django_filters.backends import DjangoFilterBackend from rest_framework_json_api.serializers import ValidationError -from api.db_utils import ProviderEnumField +from api.db_utils import ( + ProviderEnumField, + FindingDeltaEnumField, + StatusEnumField, + SeverityEnumField, +) from api.models import ( Membership, Provider, @@ -37,55 +44,6 @@ from api.uuid_utils import ( from api.v1.serializers import TaskBase -def enum_filter(queryset, value, enum_choices, lookup_field: str): - """ - Filter a queryset based on a provided value, using a specified lookup field - and validating against a given enumeration. - - This function filters a given queryset by checking if the provided `value` - matches a valid choice in the specified `enum_choices` (a Django `TextChoices` enum). - If the `value` is valid, the queryset is filtered using the specified `lookup_field`. - Otherwise, a `ValidationError` is raised. - - Args: - queryset (QuerySet): The Django queryset to be filtered. - value (str): The value to filter the queryset by, which must be a valid - option in the specified `enum_choices`. - enum_choices: The enumeration class that defines the valid - choices for the `value`. - lookup_field (str): The field or lookup path within the model used for - filtering the queryset. - - Returns: - QuerySet: A filtered queryset based on the provided `value` and `lookup_field`. - - Raises: - ValidationError: If the provided `value` is not a valid choice in - the specified `enum_choices`. - """ - if "__in" in lookup_field: - values = value - if isinstance(values, str): - values = value.split(",") - - values = [v for v in values if v in enum_choices] - return queryset.filter(**{lookup_field: values}) - - if value not in enum_choices: - raise ValidationError( - f"Invalid provider value: '{value}'. Valid values are: " - f"{', '.join(enum_choices)}" - ) - - return queryset.filter(**{lookup_field: value}) - - -def extract_lookup_expr(name): - """Extract the lookup expression from the filter name.""" - parts = name.split("__") - return parts[-1] if len(parts) > 1 else "exact" - - class CustomDjangoFilterBackend(DjangoFilterBackend): def to_html(self, _request, _queryset, _view): """Override this method to use the Browsable API in dev environments. @@ -95,6 +53,18 @@ class CustomDjangoFilterBackend(DjangoFilterBackend): return None +class UUIDInFilter(BaseInFilter, UUIDFilter): + pass + + +class CharInFilter(BaseInFilter, CharFilter): + pass + + +class ChoiceInFilter(BaseInFilter, ChoiceFilter): + pass + + class TenantFilter(FilterSet): inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date") updated_at = DateFilter(field_name="updated_at", lookup_expr="date") @@ -110,15 +80,7 @@ class TenantFilter(FilterSet): class MembershipFilter(FilterSet): date_joined = DateFilter(field_name="date_joined", lookup_expr="date") - role = CharFilter(method="filter_role") - - def filter_role(self, queryset, name, value): - return enum_filter( - queryset, - value, - enum_choices=Membership.RoleChoices, - lookup_field="role", - ) + role = ChoiceFilter(choices=Membership.RoleChoices.choices) class Meta: model = Membership @@ -133,24 +95,7 @@ class ProviderFilter(FilterSet): inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date") updated_at = DateFilter(field_name="updated_at", lookup_expr="date") connected = BooleanFilter() - provider = CharFilter(method="filter_provider_type") - provider__in = CharFilter(method="filter_provider_type_in") - - def filter_provider_type(self, queryset, name, value): - return enum_filter( - queryset, - value, - enum_choices=Provider.ProviderChoices, - lookup_field="provider", - ) - - def filter_provider_type_in(self, queryset, name, value): - return enum_filter( - queryset, - value, - enum_choices=Provider.ProviderChoices, - lookup_field="provider__in", - ) + provider = ChoiceFilter(choices=Provider.ProviderChoices.choices) class Meta: model = Provider @@ -170,81 +115,29 @@ class ProviderFilter(FilterSet): class ProviderRelationshipFilterSet(FilterSet): - provider_type = CharFilter(method="filter_provider_type") - provider_type__in = CharFilter(method="filter_provider_type_in") - provider_uid = CharFilter(method="filter_provider_uid") - provider_uid__in = CharFilter(method="filter_provider_uid_in") - provider_uid__icontains = CharFilter(method="filter_provider_uid_icontains") - provider_alias = CharFilter(method="filter_provider_alias") - provider_alias__in = CharFilter(method="filter_provider_alias_in") - provider_alias__icontains = CharFilter(method="filter_provider_alias_icontains") - - # this can be overridden in subclasses - provider_type_lookup_field = "provider__provider__exact" - provider_type_in_lookup_field = "provider__provider__in" - provider_uid_lookup_field = "provider__uid" - provider_uid_in_lookup_field = "provider__uid__in" - provider_uid_icontains_lookup_field = "provider__uid__icontains" - provider_alias_lookup_field = "provider__alias" - provider_alias_in_lookup_field = "provider__alias__in" - provider_alias_icontains_lookup_field = "provider__alias__icontains" - - def filter_provider_type(self, queryset, name, value): - return enum_filter( - queryset, - value, - enum_choices=Provider.ProviderChoices, - lookup_field=self.provider_type_lookup_field, - ) - - def filter_provider_type_in(self, queryset, name, value): - if not isinstance(value, list): - value = value.split(",") - - return enum_filter( - queryset, - value, - enum_choices=Provider.ProviderChoices, - lookup_field=self.provider_type_in_lookup_field, - ) - - def filter_provider_uid(self, queryset, name, value): - return queryset.filter(**{self.provider_uid_lookup_field: value}) - - def filter_provider_uid_in(self, queryset, name, value): - if isinstance(value, str): - value = value.split(",") - return queryset.filter(**{self.provider_uid_in_lookup_field: value}) - - def filter_provider_uid_icontains(self, queryset, name, value): - return queryset.filter(**{self.provider_uid_icontains_lookup_field: value}) - - def filter_provider_alias(self, queryset, name, value): - return queryset.filter(**{self.provider_alias_lookup_field: value}) - - def filter_provider_alias_in(self, queryset, name, value): - if isinstance(value, str): - value = value.split(",") - return queryset.filter(**{self.provider_alias_in_lookup_field: value}) - - def filter_provider_alias_icontains(self, queryset, name, value): - return queryset.filter(**{self.provider_alias_icontains_lookup_field: value}) + provider_type = ChoiceFilter( + choices=Provider.ProviderChoices.choices, field_name="provider__provider" + ) + provider_type__in = ChoiceInFilter( + choices=Provider.ProviderChoices.choices, field_name="provider__provider" + ) + provider_uid = CharFilter(field_name="provider__uid", lookup_expr="exact") + provider_uid__in = CharInFilter(field_name="provider__uid", lookup_expr="in") + provider_uid__icontains = CharFilter( + field_name="provider__uid", lookup_expr="icontains" + ) + provider_alias = CharFilter(field_name="provider__alias", lookup_expr="exact") + provider_alias__in = CharInFilter(field_name="provider__alias", lookup_expr="in") + provider_alias__icontains = CharFilter( + field_name="provider__alias", lookup_expr="icontains" + ) class ScanFilter(ProviderRelationshipFilterSet): inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date") completed_at = DateFilter(field_name="completed_at", lookup_expr="date") started_at = DateFilter(field_name="started_at", lookup_expr="date") - trigger = CharFilter(method="filter_trigger") - - def filter_trigger(self, queryset, name, value): - if value not in Scan.TriggerChoices: - raise ValidationError( - f"Invalid scan trigger value: '{value}'. Valid values are: " - f"{', '.join(Scan.TriggerChoices)}" - ) - - return queryset.filter(trigger=value) + trigger = ChoiceFilter(choices=Scan.TriggerChoices.choices) class Meta: model = Scan @@ -261,8 +154,9 @@ class TaskFilter(FilterSet): name__icontains = CharFilter( field_name="task_runner_task__task_name", lookup_expr="icontains" ) - state = CharFilter(method="filter_state", lookup_expr="exact") - + state = ChoiceFilter( + choices=StateChoices.choices, method="filter_state", lookup_expr="exact" + ) task_state_inverse_mapping_values = { v: k for k, v in TaskBase.state_mapping.items() } @@ -321,55 +215,73 @@ class ResourceFilter(ProviderRelationshipFilterSet): return queryset.filter(Q(tags__value=value) | Q(tags__value__icontains=value)) def filter_tag(self, queryset, name, value): - # we won't know what the user wants to filter on just based on the value + # We won't know what the user wants to filter on just based on the value, # and we don't want to build special filtering logic for every possible # provider tag spec, so we'll just do a full text search return queryset.filter(tags__text_search=value) -class FindingFilter(ProviderRelationshipFilterSet): +class FindingFilter(FilterSet): + # We filter providers from the scan in findings provider = UUIDFilter(field_name="scan__provider__id", lookup_expr="exact") - provider__in = UUIDFilter(method="filter_provider_id_in") - - def filter_provider_id_in(self, queryset, name, value): - if isinstance(value, str): - value = value.split(",") - if isinstance(value, UUID): - value = [value] - return queryset.filter(scan__provider__id__in=value) + provider__in = UUIDInFilter(field_name="scan__provider__id", lookup_expr="in") + provider_type = ChoiceFilter( + choices=Provider.ProviderChoices.choices, field_name="scan__provider__provider" + ) + provider_type__in = ChoiceInFilter( + choices=Provider.ProviderChoices.choices, field_name="scan__provider__provider" + ) + provider_uid = CharFilter(field_name="scan__provider__uid", lookup_expr="exact") + provider_uid__in = CharInFilter(field_name="scan__provider__uid", lookup_expr="in") + provider_uid__icontains = CharFilter( + field_name="scan__provider__uid", lookup_expr="icontains" + ) + provider_alias = CharFilter(field_name="scan__provider__alias", lookup_expr="exact") + provider_alias__in = CharInFilter( + field_name="scan__provider__alias", lookup_expr="in" + ) + provider_alias__icontains = CharFilter( + field_name="scan__provider__alias", lookup_expr="icontains" + ) updated_at = DateFilter(field_name="updated_at", lookup_expr="date") - delta = CharFilter(method="filter_delta") - delta__in = CharFilter(method="filter_delta_in") - status = CharFilter(method="filter_status") - status__in = CharFilter(method="filter_status_in") - severity = CharFilter(method="filter_severity") - severity__in = CharFilter(method="filter_severity_in") - impact = CharFilter(method="filter_severity") - impact__in = CharFilter(method="filter_severity_in") + delta = ChoiceFilter(choices=Finding.DeltaChoices.choices) + status = ChoiceFilter(choices=StatusChoices.choices) + severity = ChoiceFilter(choices=SeverityChoices) + impact = ChoiceFilter(choices=SeverityChoices) - resources = UUIDFilter(field_name="resource__id", lookup_expr="in") + resources = UUIDInFilter(field_name="resource__id", lookup_expr="in") - region = CharFilter(method="filter_region") - region__in = CharFilter(method="filter_region_in") - region__icontains = CharFilter(method="filter_region_icontains") + region = CharFilter(field_name="resources__region") + region__in = CharInFilter(field_name="resources__region", lookup_expr="in") + region__icontains = CharFilter( + field_name="resources__region", lookup_expr="icontains" + ) - service = CharFilter(method="filter_service") - service__in = CharFilter(method="filter_service_in") - service__icontains = CharFilter(method="filter_service_icontains") + service = CharFilter(field_name="resources__service") + service__in = CharInFilter(field_name="resources__service", lookup_expr="in") + service__icontains = CharFilter( + field_name="resources__service", lookup_expr="icontains" + ) - resource_uid = CharFilter(method="filter_resource_uid") - resource_uid__in = CharFilter(method="filter_resource_uid_in") - resource_uid__icontains = CharFilter(method="filter_resource_uid_icontains") + resource_uid = CharFilter(field_name="resources__uid") + resource_uid__in = CharInFilter(field_name="resources__uid", lookup_expr="in") + resource_uid__icontains = CharFilter( + field_name="resources__uid", lookup_expr="icontains" + ) - resource_name = CharFilter(method="filter_resource_name") - resource_name__in = CharFilter(method="filter_resource_name_in") - resource_name__icontains = CharFilter(method="filter_resource_name_icontains") + resource_name = CharFilter(field_name="resources__name") + resource_name__in = CharInFilter(field_name="resources__name", lookup_expr="in") + resource_name__icontains = CharFilter( + field_name="resources__name", lookup_expr="icontains" + ) - resource_type = CharFilter(method="filter_resource_type") - resource_type__in = CharFilter(method="filter_resource_type_in") - resource_type__icontains = CharFilter(method="filter_resource_type_icontains") + resource_type = CharFilter(field_name="resources__type") + resource_type__in = CharInFilter(field_name="resources__type", lookup_expr="in") + resource_type__icontains = CharFilter( + field_name="resources__type", lookup_expr="icontains" + ) scan = CharFilter(method="filter_scan_id") scan__in = CharFilter(method="filter_scan_id_in") @@ -383,20 +295,27 @@ class FindingFilter(ProviderRelationshipFilterSet): model = Finding fields = { "scan": ["exact", "in"], + "delta": ["exact", "in"], + "status": ["exact", "in"], + "severity": ["exact", "in"], + "impact": ["exact", "in"], "check_id": ["exact", "in", "icontains"], "inserted_at": ["date", "gte", "lte"], "updated_at": ["gte", "lte"], } + filter_overrides = { + FindingDeltaEnumField: { + "filter_class": CharFilter, + }, + StatusEnumField: { + "filter_class": CharFilter, + }, + SeverityEnumField: { + "filter_class": CharFilter, + }, + } def __init__(self, *args, **kwargs): - self.provider_type_lookup_field = "scan__provider__provider__exact" - self.provider_type_in_lookup_field = "scan__provider__provider__in" - self.provider_uid_lookup_field = "scan__provider__uid" - self.provider_uid_in_lookup_field = "scan__provider__uid__in" - self.provider_uid_icontains_lookup_field = "scan__provider__uid__icontains" - self.provider_alias_lookup_field = "scan__provider__alias" - self.provider_alias_in_lookup_field = "scan__provider__alias__in" - self.provider_alias_icontains_lookup_field = "scan__provider__alias__icontains" super().__init__(*args, **kwargs) data = self.data @@ -416,109 +335,6 @@ class FindingFilter(ProviderRelationshipFilterSet): self.queryset = self.queryset.filter(id__gte=start) - def filter_delta(self, queryset, name, value): - return enum_filter( - queryset, - value, - enum_choices=Finding.DeltaChoices, - lookup_field="delta", - ) - - def filter_delta_in(self, queryset, name, value): - return enum_filter( - queryset, - value, - enum_choices=Finding.DeltaChoices, - lookup_field="delta__in", - ) - - def filter_severity(self, queryset, name, value): - return enum_filter( - queryset, - value, - enum_choices=SeverityChoices, - lookup_field="severity", - ) - - def filter_severity_in(self, queryset, name, value): - return enum_filter( - queryset, - value, - enum_choices=SeverityChoices, - lookup_field="severity__in", - ) - - def filter_status(self, queryset, name, value): - return enum_filter( - queryset, - value, - enum_choices=StatusChoices, - lookup_field="status", - ) - - def filter_status_in(self, queryset, name, value): - return enum_filter( - queryset, - value, - enum_choices=StatusChoices, - lookup_field="status__in", - ) - - def filter_region(self, queryset, name, value): - return queryset.filter(resources__region=value) - - def filter_region_in(self, queryset, name, value): - if isinstance(value, str): - value = value.split(",") - return queryset.filter(resources__region__in=value) - - def filter_region_icontains(self, queryset, name, value): - return queryset.filter(resources__region__icontains=value) - - def filter_service(self, queryset, name, value): - return queryset.filter(resources__service=value) - - def filter_service_in(self, queryset, name, value): - if isinstance(value, str): - value = value.split(",") - return queryset.filter(resources__service__in=value) - - def filter_service_icontains(self, queryset, name, value): - return queryset.filter(resources__service__icontains=value) - - def filter_resource_uid(self, queryset, name, value): - return queryset.filter(resources__uid=value) - - def filter_resource_uid_in(self, queryset, name, value): - if isinstance(value, str): - value = value.split(",") - return queryset.filter(resources__uid__in=value) - - def filter_resource_uid_icontains(self, queryset, name, value): - return queryset.filter(resources__uid__icontains=value) - - def filter_resource_name(self, queryset, name, value): - return queryset.filter(resources__name=value) - - def filter_resource_name_in(self, queryset, name, value): - if isinstance(value, str): - value = value.split(",") - return queryset.filter(resources__name__in=value) - - def filter_resource_name_icontains(self, queryset, name, value): - return queryset.filter(resources__name__icontains=value) - - def filter_resource_type(self, queryset, name, value): - return queryset.filter(resources__type=value) - - def filter_resource_type_in(self, queryset, name, value): - if isinstance(value, str): - value = value.split(",") - return queryset.filter(resources__type__in=value) - - def filter_resource_type_icontains(self, queryset, name, value): - return queryset.filter(resources__type__icontains=value) - # Convert filter values to UUIDv7 values for use with partitioning def filter_scan_id(self, queryset, name, value): diff --git a/src/backend/api/middleware.py b/src/backend/api/middleware.py index d4e5d5d2c4..d648810925 100644 --- a/src/backend/api/middleware.py +++ b/src/backend/api/middleware.py @@ -5,7 +5,7 @@ from config.custom_logging import BackendLogger def extract_auth_info(request) -> dict: - if not hasattr(request, "auth") or request.auth is not None: + if getattr(request, "auth", None) is not None: tenant_id = request.auth.get("tenant_id", "N/A") user_id = request.auth.get("user_id", "N/A") else: diff --git a/src/backend/api/v1/serializers.py b/src/backend/api/v1/serializers.py index 66ffe257eb..ab14688e54 100644 --- a/src/backend/api/v1/serializers.py +++ b/src/backend/api/v1/serializers.py @@ -217,7 +217,7 @@ class UserUpdateSerializer(BaseWriteSerializer): # Tasks class TaskBase(serializers.ModelSerializer): state_mapping = { - "PENDING": StateChoices.SCHEDULED, + "PENDING": StateChoices.AVAILABLE, "STARTED": StateChoices.EXECUTING, "PROGRESS": StateChoices.EXECUTING, "SUCCESS": StateChoices.COMPLETED,