diff --git a/assets/src/bundles/add_forge/add-request-history-item.ejs b/assets/src/bundles/add_forge/add-request-history-item.ejs --- a/assets/src/bundles/add_forge/add-request-history-item.ejs +++ b/assets/src/bundles/add_forge/add-request-history-item.ejs @@ -20,6 +20,9 @@

<%= event.text %>

+ <%if (event.message_source_url !== null) { %> +

Open original message in email client

+ <% } %> <%if (event.new_status !== null) { %>

Status changed to: <%= swh.add_forge.formatRequestStatusName(event.new_status) %> diff --git a/swh/web/add_forge_now/migrations/0005_prepare_inbound_email.py b/swh/web/add_forge_now/migrations/0005_prepare_inbound_email.py new file mode 100644 --- /dev/null +++ b/swh/web/add_forge_now/migrations/0005_prepare_inbound_email.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.16 on 2022-04-01 15:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("swh_web_add_forge_now", "0004_rename_tables"), + ] + + operations = [ + migrations.AddField( + model_name="requesthistory", + name="message_source", + field=models.BinaryField(null=True), + ), + migrations.AlterField( + model_name="requesthistory", + name="actor_role", + field=models.TextField( + choices=[ + ("MODERATOR", "moderator"), + ("SUBMITTER", "submitter"), + ("FORGE_ADMIN", "forge admin"), + ("EMAIL", "email"), + ] + ), + ), + ] diff --git a/swh/web/add_forge_now/models.py b/swh/web/add_forge_now/models.py --- a/swh/web/add_forge_now/models.py +++ b/swh/web/add_forge_now/models.py @@ -67,6 +67,7 @@ MODERATOR = "moderator" SUBMITTER = "submitter" FORGE_ADMIN = "forge admin" + EMAIL = "email" @classmethod def choices(cls): @@ -85,6 +86,7 @@ actor_role = models.TextField(choices=RequestActorRole.choices()) date = models.DateTimeField(auto_now_add=True) new_status = models.TextField(choices=RequestStatus.choices(), null=True) + message_source = models.BinaryField(null=True) class Meta: app_label = APP_LABEL diff --git a/swh/web/add_forge_now/views.py b/swh/web/add_forge_now/views.py --- a/swh/web/add_forge_now/views.py +++ b/swh/web/add_forge_now/views.py @@ -5,7 +5,9 @@ from typing import Any, Dict, List +from django.conf import settings from django.conf.urls import url +from django.contrib.auth.decorators import user_passes_test from django.core.paginator import Paginator from django.db.models import Q from django.http.request import HttpRequest @@ -13,6 +15,7 @@ from django.shortcuts import render from swh.web.add_forge_now.models import Request as AddForgeRequest +from swh.web.add_forge_now.models import RequestHistory from swh.web.api.views.add_forge_now import ( AddForgeNowRequestPublicSerializer, AddForgeNowRequestSerializer, @@ -114,6 +117,32 @@ ) +@user_passes_test( + has_add_forge_now_permission, + redirect_field_name="next_path", + login_url=settings.LOGIN_URL, +) +def create_request_message_source(request: HttpRequest, id: int) -> HttpResponse: + """View to retrieve the message source for a given request history entry""" + + try: + history_entry = RequestHistory.objects.select_related("request").get( + pk=id, message_source__isnull=False + ) + assert history_entry.message_source is not None + except RequestHistory.DoesNotExist: + return HttpResponse(status=404) + + response = HttpResponse( + bytes(history_entry.message_source), content_type="text/email" + ) + filename = f"add-forge-now-{history_entry.request.forge_domain}-message{id}.eml" + + response["Content-Disposition"] = f'attachment; filename="{filename}"' + + return response + + urlpatterns = [ url( r"^add-forge/request/list/datatables/$", @@ -122,5 +151,10 @@ ), url(r"^add-forge/request/create/$", create_request_create, name="forge-add-create"), url(r"^add-forge/request/list/$", create_request_list, name="forge-add-list"), + url( + r"^add-forge/request/message-source/(?P\d+)/$", + create_request_message_source, + name="forge-add-message-source", + ), url(r"^add-forge/request/help/$", create_request_help, name="forge-add-help"), ] diff --git a/swh/web/api/views/add_forge_now.py b/swh/web/api/views/add_forge_now.py --- a/swh/web/api/views/add_forge_now.py +++ b/swh/web/api/views/add_forge_now.py @@ -107,9 +107,21 @@ class AddForgeNowRequestHistorySerializer(serializers.ModelSerializer): + message_source_url = serializers.SerializerMethodField() + class Meta: model = AddForgeNowRequestHistory - exclude = ("request",) + exclude = ("request", "message_source") + + def get_message_source_url(self, request_history): + if request_history.message_source is None: + return None + + return reverse( + "forge-add-message-source", + url_args={"id": request_history.pk}, + request=self.context["request"], + ) class AddForgeNowRequestHistoryPublicSerializer(serializers.ModelSerializer): @@ -388,7 +400,9 @@ ADD_FORGE_MODERATOR_PERMISSION ): data = AddForgeNowRequestSerializer(add_forge_request).data - history = AddForgeNowRequestHistorySerializer(request_history, many=True).data + history = AddForgeNowRequestHistorySerializer( + request_history, many=True, context={"request": request} + ).data else: data = AddForgeNowRequestPublicSerializer(add_forge_request).data history = AddForgeNowRequestHistoryPublicSerializer( diff --git a/swh/web/tests/api/views/test_add_forge_now.py b/swh/web/tests/api/views/test_add_forge_now.py --- a/swh/web/tests/api/views/test_add_forge_now.py +++ b/swh/web/tests/api/views/test_add_forge_now.py @@ -12,13 +12,14 @@ import iso8601 import pytest -from swh.web.add_forge_now.models import Request +from swh.web.add_forge_now.models import Request, RequestHistory from swh.web.common.utils import reverse from swh.web.config import get_config from swh.web.inbound_email.utils import get_address_for_pk from swh.web.tests.utils import ( check_api_get_responses, check_api_post_response, + check_http_get_response, check_http_post_response, ) @@ -550,6 +551,7 @@ "actor_role": "SUBMITTER", "date": resp.data["history"][0]["date"], "new_status": "PENDING", + "message_source_url": None, }, { "id": 2, @@ -558,11 +560,69 @@ "actor_role": "MODERATOR", "date": resp.data["history"][1]["date"], "new_status": "WAITING_FOR_FEEDBACK", + "message_source_url": None, }, ], } +@pytest.mark.django_db(transaction=True, reset_sequences=True) +def test_add_forge_request_get_moderator_message_source( + api_client, regular_user, add_forge_moderator +): + resp = create_add_forge_request(api_client, regular_user) + + rh = RequestHistory( + request=Request.objects.get(pk=resp.data["id"]), + new_status="WAITING_FOR_FEEDBACK", + text="waiting for message", + actor=add_forge_moderator.username, + actor_role="MODERATOR", + message_source=b"test with a message source", + ) + rh.save() + + api_client.force_login(add_forge_moderator) + url = reverse("api-1-add-forge-request-get", url_args={"id": resp.data["id"]}) + resp = check_api_get_responses(api_client, url, status_code=200) + resp.data["history"] = [dict(history_item) for history_item in resp.data["history"]] + + # Check that the authentified moderator can't urlhack non-existent message sources + assert resp.data["history"][0]["message_source_url"] is None + empty_message_url = reverse( + "forge-add-message-source", url_args={"id": resp.data["history"][0]["id"]} + ) + check_http_get_response(api_client, empty_message_url, status_code=404) + + # Check that the authentified moderator can't urlhack non-existent message sources + non_existent_message_url = reverse( + "forge-add-message-source", url_args={"id": 9001} + ) + check_http_get_response(api_client, non_existent_message_url, status_code=404) + + # Check that the authentified moderator can access the message source when the url is + # given + + message_source_url = resp.data["history"][-1]["message_source_url"] + assert message_source_url is not None + + message_source_resp = check_http_get_response( + api_client, message_source_url, status_code=200, content_type="text/email" + ) + + # Check that the message source shows up as an attachment + assert message_source_resp.content == rh.message_source + disposition = message_source_resp["Content-Disposition"] + assert disposition.startswith("attachment; filename=") + assert disposition.endswith('.eml"') + + # Check that a regular user can't access message sources + api_client.force_login(regular_user) + check_http_get_response(api_client, message_source_url, status_code=302) + + api_client.force_login(add_forge_moderator) + + @pytest.mark.django_db(transaction=True, reset_sequences=True) def test_add_forge_request_get_invalid(api_client): url = reverse("api-1-add-forge-request-get", url_args={"id": 3})