Page MenuHomeSoftware Heritage

D7326.id26549.diff
No OneTemporary

D7326.id26549.diff

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
@@ -8,7 +8,17 @@
import enum
from typing import List
+from django.core.signing import Signer
from django.db import models
+from django.utils.crypto import constant_time_compare
+
+# signal_receivers imports this module, don't expand this import or you'll get a
+# circular import
+from . import signal_receivers
+from ..config import get_config
+from ..inbound_email.signals import email_received
+
+email_received.connect(signal_receivers.handle_inbound_message)
class RequestStatus(enum.Enum):
@@ -97,3 +107,40 @@
forge_contact_comment = models.TextField(
help_text="Where did you find this contact information (url, ...)",
)
+
+ ADDRESS_SIGNER_SEP = "."
+
+ @classmethod
+ def get_signer(cls):
+ return Signer(salt="add_forge_now_request", sep=cls.ADDRESS_SIGNER_SEP)
+
+ def get_inbound_address(self):
+ """Get the email address that will be able to receive messages to be logged in
+ this request."""
+
+ base_address = get_config()["add_forge_now"]["email_address"]
+
+ username, domain = base_address.split("@")
+
+ extension = self.get_signer().sign(self.pk)
+
+ return f"{username}+{extension}@{domain}"
+
+ @classmethod
+ def verify_address_extension(cls, extension: str) -> int:
+ """Retrieve the primary key of the request for the given inbound address extension.
+
+ We reimplement `Signer.unsign`, because the extension can be casemapped at any
+ point in the email chain (even though email is, theoretically, case sensitive),
+ so we have to compare lowercase versions of both the extension and the
+ signature...
+
+ Raises ValueError if the signature couldn't be verified.
+
+ """
+ value, signature = extension.rsplit(cls.ADDRESS_SIGNER_SEP, 1)
+ expected_signature = cls.get_signer().signature(value)
+ if not constant_time_compare(signature.lower(), expected_signature.lower()):
+ raise ValueError(f"Invalid signature in extension {extension}")
+
+ return int(value)
diff --git a/swh/web/add_forge_now/signal_receivers.py b/swh/web/add_forge_now/signal_receivers.py
new file mode 100644
--- /dev/null
+++ b/swh/web/add_forge_now/signal_receivers.py
@@ -0,0 +1,76 @@
+# Copyright (C) 2022 The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU Affero General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+from email.message import EmailMessage
+import logging
+from typing import Optional, Type
+
+# models imports this module, don't expand this import or you'll get a circular import
+from . import models
+from ..config import get_config
+from ..inbound_email.signals import EmailProcessingStatus
+from ..inbound_email.utils import recipient_matches
+
+logger = logging.getLogger(__name__)
+
+
+def handle_inbound_message(sender: Type, **kwargs) -> EmailProcessingStatus:
+ """Handle inbound email messages for add forge now.
+
+ # How do we determine that the inbound message is for us (To/Cc/Received)?
+
+ # How do we extract the RequestHistory fields out of the email:
+ # - text
+ # - actor
+ # - actor_role
+ # - new_status (?)
+
+ """
+ message = kwargs["message"]
+ assert isinstance(message, EmailMessage)
+
+ match_address = get_config()["add_forge_now"]["email_address"]
+
+ matches = recipient_matches(message, match_address)
+ if not matches:
+ return EmailProcessingStatus.IGNORED
+
+ request_pk: Optional[int] = None
+ for match in matches:
+ extension = match.extension
+ if extension is None:
+ logger.debug(
+ "Recipient address %s cannot be matched to a request, ignoring",
+ match.recipient.addr_spec,
+ )
+ continue
+ try:
+ current_pk = models.Request.verify_address_extension(extension)
+ except ValueError:
+ logger.debug(
+ "Recipient address %s failed validation", match.recipient.addr_spec
+ )
+ continue
+
+ if request_pk is not None and request_pk != current_pk:
+ logger.debug(
+ "Recipient address %s inconsistent with earlier valid address with "
+ "pk=%s, failing",
+ match.recipient.addr_spec,
+ request_pk,
+ )
+ return EmailProcessingStatus.FAILED
+
+ request_pk = current_pk
+
+ if request_pk is None:
+ # No matches had a valid extension, unable to process message
+ return EmailProcessingStatus.FAILED
+
+ request = models.Request.objects.get(pk=request_pk)
+
+ print(request)
+
+ return EmailProcessingStatus.PROCESSED
diff --git a/swh/web/config.py b/swh/web/config.py
--- a/swh/web/config.py
+++ b/swh/web/config.py
@@ -127,6 +127,7 @@
"staging_server_names": ("list", SWH_WEB_STAGING_SERVER_NAMES),
"instance_name": ("str", "archive-test.softwareheritage.org"),
"give": ("dict", {"public_key": "", "token": ""}),
+ "add_forge_now": ("dict", {"email_address": "add-forge-now@example.com"}),
}
swhweb_config: Dict[str, Any] = {}

File Metadata

Mime Type
text/plain
Expires
Dec 21 2024, 5:42 PM (11 w, 4 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3229381

Event Timeline