--- /dev/null
+# Generated by Django 5.1.6 on 2025-02-16 16:31
+
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("documents", "1062_alter_savedviewfilterrule_rule_type"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="workflowactionwebhook",
+ name="url",
+ field=models.CharField(
+ help_text="The destination URL for the notification.",
+ max_length=256,
+ verbose_name="webhook url",
+ ),
+ ),
+ ]
class WorkflowActionWebhook(models.Model):
- url = models.URLField(
+ # We dont use the built-in URLField because it is not flexible enough
+ # validation is handled in the serializer
+ url = models.CharField(
_("webhook url"),
null=False,
+ max_length=256,
help_text=_("The destination URL for the notification."),
)
from documents.templating.filepath import validate_filepath_template_and_render
from documents.templating.utils import convert_format_str_to_template_format
from documents.validators import uri_validator
+from documents.validators import url_validator
logger = logging.getLogger("paperless.serializers")
class WorkflowActionWebhookSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(allow_null=True, required=False)
+ def validate_url(self, url):
+ url_validator(url)
+ return url
+
class Meta:
model = WorkflowActionWebhook
fields = [
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ def test_webhook_action_url_validation(self):
+ """
+ GIVEN:
+ - API request to create a workflow with a notification action
+ WHEN:
+ - API is called
+ THEN:
+ - Correct HTTP response
+ """
+
+ for url, expected_resp_code in [
+ ("https://examplewithouttld:3000/path", status.HTTP_201_CREATED),
+ ("file:///etc/passwd/path", status.HTTP_400_BAD_REQUEST),
+ ]:
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "name": "Workflow 2",
+ "order": 1,
+ "triggers": [
+ {
+ "type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
+ "sources": [DocumentSource.ApiUpload],
+ "filter_filename": "*",
+ },
+ ],
+ "actions": [
+ {
+ "type": WorkflowAction.WorkflowActionType.WEBHOOK,
+ "webhook": {
+ "url": url,
+ "include_document": False,
+ },
+ },
+ ],
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, expected_resp_code)
from django.utils.translation import gettext_lazy as _
-def uri_validator(value) -> None:
+def uri_validator(value: str, allowed_schemes: set[str] | None = None) -> None:
"""
- Raises a ValidationError if the given value does not parse as an
- URI looking thing, which we're defining as a scheme and either network
- location or path value
+ Validates that the given value parses as a URI with required components
+ and optionally restricts to specific schemes.
+
+ Args:
+ value: The URI string to validate
+ allowed_schemes: Optional set/list of allowed schemes (e.g. {'http', 'https'}).
+ If None, all schemes are allowed.
+
+ Raises:
+ ValidationError: If the URI is invalid or uses a disallowed scheme
"""
try:
parts = urlparse(value)
_(f"Unable to parse URI {value}, missing net location or path"),
params={"value": value},
)
+
+ if allowed_schemes and parts.scheme not in allowed_schemes:
+ raise ValidationError(
+ _(
+ f"URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}",
+ ),
+ params={"value": value, "scheme": parts.scheme},
+ )
+
+ except ValidationError:
+ raise
except Exception as e:
raise ValidationError(
_(f"Unable to parse URI {value}"),
params={"value": value},
) from e
+
+
+def url_validator(value) -> None:
+ """
+ Validates that the given value is a valid HTTP or HTTPS URL.
+
+ Args:
+ value: The URL string to validate
+
+ Raises:
+ ValidationError: If the URL is invalid or not using http/https scheme
+ """
+ uri_validator(value, allowed_schemes={"http", "https"})