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
7 changes: 5 additions & 2 deletions dojo/asset/api/serializers.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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:
Expand Down
47 changes: 47 additions & 0 deletions dojo/authorization/serializer_guards.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
32 changes: 5 additions & 27 deletions dojo/product/api/serializer.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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()
Expand Down
45 changes: 44 additions & 1 deletion unittests/test_api_scan_configuration_tool_authz.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@
set, and a submitted pk is rejected, so narrowing the rendered <select> alone
cannot be bypassed by POSTing the id directly, and the REST endpoint cannot be
used to attach an unauthorized configuration either.

The V3 asset alias is a second serializer over the same model, so it is covered
here too: a member of the asset who lacks the permission cannot attach an
unauthorized configuration through the alias, on create or on update.
"""
from types import SimpleNamespace

from crum import impersonate
from rest_framework.exceptions import PermissionDenied

from dojo.models import Dojo_User, Tool_Configuration, Tool_Type
from dojo.asset.api.serializers import AssetAPIScanConfigurationSerializer
from dojo.models import Dojo_User, Product_API_Scan_Configuration, Tool_Configuration, Tool_Type
from dojo.product.api.serializer import ProductAPIScanConfigurationSerializer
from dojo.product.ui.forms import Product_API_Scan_ConfigurationForm

Expand All @@ -29,6 +35,10 @@ def setUp(self):
name="prod-sonarqube", tool_type=tool_type, authentication_type="API",
url="http://example.invalid/api", api_key="ADMIN-TOKEN",
)
self.other_tool_config = Tool_Configuration.objects.create(
name="prod-sonarqube-secondary", tool_type=tool_type, authentication_type="API",
url="http://example.invalid/api2", api_key="ADMIN-TOKEN-2",
)
self.unprivileged = Dojo_User.objects.create(
username="scanconf_unprivileged", is_staff=False, is_superuser=False,
)
Expand Down Expand Up @@ -72,3 +82,36 @@ def test_rest_rejects_unauthorized_tool_configuration(self):
def test_rest_allows_authorized_tool_configuration(self):
serializer = self._serializer(self.staff)
self.assertTrue(serializer.is_valid(), serializer.errors)

def _asset_for(self, user, name):
product = self.create_product(name, prod_type=self.product_type)
product.authorized_users.add(user)
return product

def _alias_serializer(self, user, product, **kwargs):
return AssetAPIScanConfigurationSerializer(
data={"asset": product.pk, "tool_configuration": self.tool_config.pk, "service_key_1": "k1"},
context={"request": SimpleNamespace(user=user)},
**kwargs,
)

def test_alias_rest_rejects_unauthorized_tool_configuration(self):
product = self._asset_for(self.unprivileged, "scanconf-alias-unprivileged")
with impersonate(self.unprivileged), self.assertRaises(PermissionDenied):
self._alias_serializer(self.unprivileged, product).is_valid(raise_exception=True)

def test_alias_rest_rejects_unauthorized_tool_configuration_on_update(self):
product = self._asset_for(self.unprivileged, "scanconf-alias-unprivileged-update")
existing = Product_API_Scan_Configuration.objects.create(
product=product, tool_configuration=self.other_tool_config, service_key_1="k0",
)
with impersonate(self.unprivileged), self.assertRaises(PermissionDenied):
self._alias_serializer(
self.unprivileged, product, instance=existing, partial=True,
).is_valid(raise_exception=True)

def test_alias_rest_allows_authorized_tool_configuration(self):
product = self._asset_for(self.staff, "scanconf-alias-staff")
with impersonate(self.staff):
serializer = self._alias_serializer(self.staff, product)
self.assertTrue(serializer.is_valid(), serializer.errors)
Loading