diff --git a/dojo/asset/api/serializers.py b/dojo/asset/api/serializers.py index baaea888379..849822e79f2 100644 --- a/dojo/asset/api/serializers.py +++ b/dojo/asset/api/serializers.py @@ -1,7 +1,10 @@ from rest_framework import serializers from dojo.api_v2.serializers import ProductMetaSerializer, TagListSerializerField -from dojo.authorization.serializer_guards import AuthorizedUsersMemberGuardMixin +from dojo.authorization.serializer_guards import ( + AuthorizedUsersMemberGuardMixin, + ToolConfigurationUseGuardMixin, +) from dojo.models import ( Dojo_User, Product, @@ -16,7 +19,7 @@ def get_queryset(self): return get_authorized_products("view") -class AssetAPIScanConfigurationSerializer(serializers.ModelSerializer): +class AssetAPIScanConfigurationSerializer(ToolConfigurationUseGuardMixin, serializers.ModelSerializer): asset = RelatedAssetField(source="product") class Meta: diff --git a/dojo/authorization/serializer_guards.py b/dojo/authorization/serializer_guards.py index 69720de238e..6ec2532adef 100644 --- a/dojo/authorization/serializer_guards.py +++ b/dojo/authorization/serializer_guards.py @@ -76,3 +76,50 @@ def _validate_authorized_users_change(self, data): if not (request_user and user_has_permission(request_user, self.instance, permission)): msg = f"You do not have permission to manage authorized users for this {label}." raise PermissionDenied(msg) + + +class ToolConfigurationUseGuardMixin: + + """ + Enforce ``view_tool_configuration`` on ``tool_configuration`` writes. + + Selecting a ``tool_configuration`` lets an import run authenticated requests + with the credential stored on it, so it is gated by the same + ``view_tool_configuration`` permission that guards the tool-configuration + views, not just the object permission these endpoints already check. + + Mix this into *every* serializer exposing the field, including alias + serializers over the same model, so the rule holds wherever the field is + reachable. + + The check hangs off ``run_validation`` rather than ``validate`` on purpose: a + subclass that defines its own ``validate`` would otherwise shadow the mixin's + and silently drop the guard. + + No-ops when the field is absent (replay-safe on PATCH), mirroring + dojo.authorization.api_permissions.check_update_permission. + """ + + def run_validation(self, data=serializers.empty): + value = super().run_validation(data) + self._validate_tool_configuration_use(value) + return value + + def _validate_tool_configuration_use(self, data): + if "tool_configuration" not in data: + return + + from dojo.tool_config.queries import ( # noqa: PLC0415 -- lazy import, avoids circular dependency + get_authorized_tool_configurations, + ) + + # Field-level validation has already resolved the payload to a + # Tool_Configuration instance at this point. + tool_configuration = data.get("tool_configuration") + if tool_configuration is None: + return + request = self.context.get("request") + request_user = getattr(request, "user", None) + if not get_authorized_tool_configurations(request_user).filter(pk=tool_configuration.pk).exists(): + msg = "You do not have permission to use this tool configuration." + raise PermissionDenied(msg) diff --git a/dojo/product/api/serializer.py b/dojo/product/api/serializer.py index ccb52dfa273..56e37d0c3a7 100644 --- a/dojo/product/api/serializer.py +++ b/dojo/product/api/serializer.py @@ -1,9 +1,10 @@ from rest_framework import serializers -from rest_framework.exceptions import PermissionDenied -from dojo.authorization.serializer_guards import AuthorizedUsersMemberGuardMixin +from dojo.authorization.serializer_guards import ( + AuthorizedUsersMemberGuardMixin, + ToolConfigurationUseGuardMixin, +) from dojo.models import DojoMeta, Product, Product_API_Scan_Configuration -from dojo.tool_config.queries import get_authorized_tool_configurations class ProductMetaSerializer(serializers.ModelSerializer): @@ -12,34 +13,11 @@ class Meta: fields = ("name", "value") -class ProductAPIScanConfigurationSerializer(serializers.ModelSerializer): +class ProductAPIScanConfigurationSerializer(ToolConfigurationUseGuardMixin, serializers.ModelSerializer): class Meta: model = Product_API_Scan_Configuration fields = "__all__" - def validate(self, data): - self._validate_tool_configuration_use(data) - return data - - def _validate_tool_configuration_use(self, data): - """ - Selecting a ``tool_configuration`` lets an import run authenticated - requests with the credential stored on it, so it is gated by the same - ``view_tool_configuration`` permission that guards the tool-configuration - views -- not just the product permission this endpoint already checks. - - No-ops when the field is absent (replay-safe on PATCH), mirroring - dojo.authorization.api_permissions.check_update_permission. - """ - if "tool_configuration" not in data: - return - tool_configuration = data.get("tool_configuration") - request = self.context.get("request") - request_user = getattr(request, "user", None) - if tool_configuration is not None and not get_authorized_tool_configurations(request_user).filter(pk=tool_configuration.pk).exists(): - msg = "You do not have permission to use this tool configuration." - raise PermissionDenied(msg) - class ProductSerializer(AuthorizedUsersMemberGuardMixin, serializers.ModelSerializer): findings_count = serializers.SerializerMethodField() diff --git a/dojo/tool_product/api/serializer.py b/dojo/tool_product/api/serializer.py index 12aafcdfec7..72339131805 100644 --- a/dojo/tool_product/api/serializer.py +++ b/dojo/tool_product/api/serializer.py @@ -1,10 +1,11 @@ from rest_framework import serializers +from dojo.authorization.serializer_guards import ToolConfigurationUseGuardMixin from dojo.models import Product from dojo.tool_product.models import Tool_Product_Settings -class ToolProductSettingsSerializer(serializers.ModelSerializer): +class ToolProductSettingsSerializer(ToolConfigurationUseGuardMixin, serializers.ModelSerializer): setting_url = serializers.CharField(source="url") product = serializers.PrimaryKeyRelatedField( queryset=Product.objects.all(), required=True, diff --git a/dojo/tool_product/ui/forms.py b/dojo/tool_product/ui/forms.py index f62dac4bb9c..c60565cd989 100644 --- a/dojo/tool_product/ui/forms.py +++ b/dojo/tool_product/ui/forms.py @@ -2,6 +2,7 @@ from django.core.validators import URLValidator from dojo.tool_config.models import Tool_Configuration +from dojo.tool_config.queries import get_authorized_tool_configurations from dojo.tool_product.models import Tool_Product_Settings @@ -15,7 +16,11 @@ class Meta: class ToolProductSettingsForm(forms.ModelForm): - tool_configuration = forms.ModelChoiceField(queryset=Tool_Configuration.objects.all(), label="Tool Configuration") + tool_configuration = forms.ModelChoiceField(queryset=Tool_Configuration.objects.none(), label="Tool Configuration") + + def __init__(self, *args, user=None, **kwargs): + super().__init__(*args, **kwargs) + self.fields["tool_configuration"].queryset = get_authorized_tool_configurations(user) class Meta: model = Tool_Product_Settings diff --git a/dojo/tool_product/ui/views.py b/dojo/tool_product/ui/views.py index 39afab79e28..c77ea34f31c 100644 --- a/dojo/tool_product/ui/views.py +++ b/dojo/tool_product/ui/views.py @@ -19,7 +19,7 @@ def new_tool_product(request, pid): prod = get_object_or_404(Product, id=pid) if request.method == "POST": - tform = ToolProductSettingsForm(request.POST) + tform = ToolProductSettingsForm(request.POST, user=request.user) if tform.is_valid(): # form.tool_type = tool_type new_prod = tform.save(commit=False) @@ -34,7 +34,7 @@ def new_tool_product(request, pid): return HttpResponseRedirect( reverse("all_tool_product", args=(pid, ))) else: - tform = ToolProductSettingsForm() + tform = ToolProductSettingsForm(user=request.user) product_tab = Product_Tab(prod, title=_("Tool Configurations"), tab="settings") return render(request, "dojo/new_tool_product.html", { "tform": tform, @@ -61,7 +61,7 @@ def edit_tool_product(request, pid, ttid): raise PermissionDenied if request.method == "POST": - tform = ToolProductSettingsForm(request.POST, instance=tool_product) + tform = ToolProductSettingsForm(request.POST, instance=tool_product, user=request.user) if tform.is_valid(): tform.save() messages.add_message( @@ -71,7 +71,7 @@ def edit_tool_product(request, pid, ttid): extra_tags="alert-success") return HttpResponseRedirect(reverse("all_tool_product", args=(pid, ))) else: - tform = ToolProductSettingsForm(instance=tool_product) + tform = ToolProductSettingsForm(instance=tool_product, user=request.user) product_tab = Product_Tab(product, title=_("Edit Product Tool Configuration"), tab="settings") return render(request, "dojo/edit_tool_product.html", { @@ -95,7 +95,7 @@ def delete_tool_product(request, pid, ttid): _("Tool Product Successfully Deleted."), extra_tags="alert-success") return HttpResponseRedirect(reverse("all_tool_product", args=(pid, ))) - tform = ToolProductSettingsForm(instance=tool_product) + tform = ToolProductSettingsForm(instance=tool_product, user=request.user) product_tab = Product_Tab(product, title=_("Delete Product Tool Configuration"), tab="settings") diff --git a/unittests/test_api_scan_configuration_tool_authz.py b/unittests/test_api_scan_configuration_tool_authz.py index 8508db40c46..d405e5753bb 100644 --- a/unittests/test_api_scan_configuration_tool_authz.py +++ b/unittests/test_api_scan_configuration_tool_authz.py @@ -10,14 +10,22 @@ set, and a submitted pk is rejected, so narrowing the rendered