Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions dojo/tool_config/queries.py
Original file line number Diff line number Diff line change
@@ -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()
14 changes: 14 additions & 0 deletions dojo/tool_product/api/serializer.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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
7 changes: 6 additions & 1 deletion dojo/tool_product/ui/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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
Expand Down
10 changes: 5 additions & 5 deletions dojo/tool_product/ui/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
Expand All @@ -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(
Expand All @@ -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", {
Expand All @@ -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")

Expand Down
70 changes: 70 additions & 0 deletions unittests/test_tool_product_settings_tool_authz.py
Original file line number Diff line number Diff line change
@@ -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 <select> cannot be bypassed by POSTing the id -- via the UI or the API.
Mirrors unittests/test_api_scan_configuration_tool_authz.py.
"""
from types import SimpleNamespace

from rest_framework.exceptions import PermissionDenied

from dojo.models import Dojo_User, Tool_Configuration, Tool_Type
from dojo.tool_product.api.serializer import ToolProductSettingsSerializer
from dojo.tool_product.ui.forms import ToolProductSettingsForm

from .dojo_test_case import DojoTestCase


class ToolProductSettingsToolAuthzTest(DojoTestCase):
def setUp(self):
tool_type, _ = Tool_Type.objects.get_or_create(name="SonarQube")
self.tool_config = Tool_Configuration.objects.create(
name="prod-sonarqube", tool_type=tool_type, authentication_type="API",
url="http://example.invalid/api", api_key="ADMIN-TOKEN",
)
self.unprivileged = Dojo_User.objects.create(
username="toolprod_unprivileged", is_staff=False, is_superuser=False,
)
self.staff = Dojo_User.objects.create(
username="toolprod_staff", is_staff=True, is_superuser=False,
)
self.product_type = self.create_product_type("toolprod-org")

def _post(self, user):
return ToolProductSettingsForm(
{"name": "setting", "tool_configuration": self.tool_config.pk, "tool_project_id": "1"},
user=user,
)

def test_unprivileged_user_is_offered_no_tool_configurations(self):
form = ToolProductSettingsForm(user=self.unprivileged)
self.assertNotIn(self.tool_config, form.fields["tool_configuration"].queryset)

def test_unprivileged_user_cannot_submit_a_tool_configuration(self):
form = self._post(self.unprivileged)
self.assertFalse(form.is_valid())
self.assertIn("tool_configuration", form.errors)

def test_privileged_user_can_select_the_tool_configuration(self):
form = self._post(self.staff)
self.assertIn(self.tool_config, form.fields["tool_configuration"].queryset)
self.assertNotIn("tool_configuration", form.errors)

def _serializer(self, user):
product = self.create_product("toolprod-product", prod_type=self.product_type)
return ToolProductSettingsSerializer(
data={
"product": product.pk, "tool_configuration": self.tool_config.pk,
"name": "setting", "setting_url": "http://www.example.com",
},
context={"request": SimpleNamespace(user=user)},
)

def test_rest_rejects_unauthorized_tool_configuration(self):
serializer = self._serializer(self.unprivileged)
with self.assertRaises(PermissionDenied):
serializer.is_valid(raise_exception=True)

def test_rest_allows_authorized_tool_configuration(self):
serializer = self._serializer(self.staff)
self.assertTrue(serializer.is_valid(), serializer.errors)
Loading