From 063f7a11f9cbcda2bc449a67c014f5247a398fe9 Mon Sep 17 00:00:00 2001 From: svader0 Date: Fri, 31 Jul 2026 12:24:21 -0500 Subject: [PATCH] Enforce tool-configuration permission on Tool Product Settings selection ToolProductSettingsForm offered every Tool_Configuration regardless of the user's permissions, and ToolProductSettingsSerializer (fields="__all__") accepted any tool_configuration id. Tool configurations hold shared third-party credentials gated by view_tool_configuration, so a user who cannot view them must not be able to select or submit one -- via the UI or the REST API. Same authorization gap as the API scan configuration fix (H1 #3900540), lower risk here since tool_product does not replay the credential, but closed for consistency. Scope the form field queryset via get_authorized_tool_configurations(user) (passed from all tool_product views) and validate the same field in the serializer. Reuses the helper added for #3900540. Adds a regression test. Co-Authored-By: Claude Opus 4.8 --- dojo/tool_config/queries.py | 12 ++++ dojo/tool_product/api/serializer.py | 14 ++++ dojo/tool_product/ui/forms.py | 7 +- dojo/tool_product/ui/views.py | 10 +-- .../test_tool_product_settings_tool_authz.py | 70 +++++++++++++++++++ 5 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 dojo/tool_config/queries.py create mode 100644 unittests/test_tool_product_settings_tool_authz.py diff --git a/dojo/tool_config/queries.py b/dojo/tool_config/queries.py new file mode 100644 index 00000000000..f7974307878 --- /dev/null +++ b/dojo/tool_config/queries.py @@ -0,0 +1,12 @@ +from dojo.authorization.authorization import user_has_configuration_permission +from dojo.tool_config.models import Tool_Configuration + + +def get_authorized_tool_configurations(user): + """ + Configs the user may select; empty queryset (not an error) if they lack view_tool_configuration. + Back a ModelChoiceField with it so the accepted POST value is gated too, not just the rendered choices. + """ + if user_has_configuration_permission(user, "dojo.view_tool_configuration"): + return Tool_Configuration.objects.all().order_by("name") + return Tool_Configuration.objects.none() diff --git a/dojo/tool_product/api/serializer.py b/dojo/tool_product/api/serializer.py index 12aafcdfec7..eb56cc9d262 100644 --- a/dojo/tool_product/api/serializer.py +++ b/dojo/tool_product/api/serializer.py @@ -1,6 +1,8 @@ from rest_framework import serializers +from rest_framework.exceptions import PermissionDenied from dojo.models import Product +from dojo.tool_config.queries import get_authorized_tool_configurations from dojo.tool_product.models import Tool_Product_Settings @@ -13,3 +15,15 @@ class ToolProductSettingsSerializer(serializers.ModelSerializer): class Meta: model = Tool_Product_Settings fields = "__all__" + + def validate(self, data): + # A tool_configuration binds a stored third-party credential, so gate it by + # view_tool_configuration (the same permission the tool-config views enforce), + # not just the product permission this endpoint checks. Absent on PATCH -> no-op. + tool_configuration = data.get("tool_configuration") + if tool_configuration is not None: + request_user = getattr(self.context.get("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) + return data 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_tool_product_settings_tool_authz.py b/unittests/test_tool_product_settings_tool_authz.py new file mode 100644 index 00000000000..8cd3599ff2e --- /dev/null +++ b/unittests/test_tool_product_settings_tool_authz.py @@ -0,0 +1,70 @@ +""" +Regression: the Tool Product Settings form and REST serializer must not offer or +accept a Tool_Configuration the user lacks ``view_tool_configuration`` for, so a +narrowed