This is an automated email from the ASF dual-hosted git repository.

arm pushed a commit to branch arm
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/arm by this push:
     new 0c467bb2 Add LDAP validation to ASF sender IDs. Closes #654.
0c467bb2 is described below

commit 0c467bb24bbaa07dac96ab5925d73e55a2eacba1
Author: Alastair McFarlane <[email protected]>
AuthorDate: Tue Feb 17 10:28:02 2026 +0000

    Add LDAP validation to ASF sender IDs. Closes #654.
---
 atr/tasks/message.py       |   7 +++
 tests/unit/test_message.py | 111 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 118 insertions(+)

diff --git a/atr/tasks/message.py b/atr/tasks/message.py
index 4e596032..b7bfe3b6 100644
--- a/atr/tasks/message.py
+++ b/atr/tasks/message.py
@@ -15,6 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 
+import atr.ldap as ldap
 import atr.log as log
 import atr.mail as mail
 import atr.models.results as results
@@ -47,6 +48,12 @@ async def send(args: Send) -> results.Results | None:
     else:
         raise SendError(f"Invalid email sender: {args.email_sender}")
 
+    sender_account = await ldap.account_lookup(sender_asf_uid)
+    if sender_account is None:
+        raise SendError(f"Invalid email account: {args.email_sender}")
+    if ldap.is_banned(sender_account):
+        raise SendError(f"Email account {args.email_sender} is banned")
+
     recipient_domain = args.email_recipient.split("@")[-1]
     sending_to_self = recipient_domain == f"{sender_asf_uid}@apache.org"
     sending_to_committee = recipient_domain.endswith(".apache.org")
diff --git a/tests/unit/test_message.py b/tests/unit/test_message.py
new file mode 100644
index 00000000..b5e86281
--- /dev/null
+++ b/tests/unit/test_message.py
@@ -0,0 +1,111 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Tests for ASF ID validation in atr.tasks.message module."""
+
+import contextlib
+from typing import TYPE_CHECKING
+from unittest.mock import AsyncMock, MagicMock
+
+import pytest
+
+import atr.tasks.message as message
+
+if TYPE_CHECKING:
+    from pytest import MonkeyPatch
+
+
+def _send_args(
+    email_sender: str = "[email protected]",
+    email_recipient: str = "[email protected]",
+    subject: str = "Test Subject",
+    body: str = "Test body",
+    in_reply_to: str | None = None,
+) -> dict[str, str | None]:
+    """Build an argument dict matching the Send schema."""
+    return {
+        "email_sender": email_sender,
+        "email_recipient": email_recipient,
+        "subject": subject,
+        "body": body,
+        "in_reply_to": in_reply_to,
+    }
+
+
[email protected]
+async def test_send_rejects_invalid_asf_id(monkeypatch: "MonkeyPatch") -> None:
+    """Test that an ASF UID not found in LDAP raises SendError."""
+    # ldap.account_lookup returns None for an unknown UID
+    monkeypatch.setattr("atr.tasks.message.ldap.account_lookup", 
AsyncMock(return_value=None))
+
+    with pytest.raises(message.SendError, match=r"Invalid email account"):
+        await message.send(_send_args(email_sender="[email protected]"))
+
+
[email protected]
+async def test_send_rejects_bare_invalid_asf_id(monkeypatch: "MonkeyPatch") -> 
None:
+    """Test that a bare ASF UID (no @) not found in LDAP raises SendError."""
+    monkeypatch.setattr("atr.tasks.message.ldap.account_lookup", 
AsyncMock(return_value=None))
+
+    with pytest.raises(message.SendError, match=r"Invalid email account"):
+        await message.send(_send_args(email_sender="nosuchuser"))
+
+
[email protected]
+async def test_send_rejects_banned_asf_account(monkeypatch: "MonkeyPatch") -> 
None:
+    """Test that a banned ASF account raises SendError."""
+    monkeypatch.setattr(
+        "atr.tasks.message.ldap.account_lookup",
+        AsyncMock(return_value={"uid": "banneduser", "cn": "Banned User", 
"asf-banned": "yes"}),
+    )
+
+    with pytest.raises(message.SendError, match=r"banned"):
+        await message.send(_send_args(email_sender="[email protected]"))
+
+
[email protected]
+async def test_send_succeeds_with_valid_asf_id(monkeypatch: "MonkeyPatch") -> 
None:
+    """Test that a valid ASF UID passes LDAP validation and sends the email."""
+    # ldap.account_lookup returns a dict for a known UID
+    monkeypatch.setattr(
+        "atr.tasks.message.ldap.account_lookup",
+        AsyncMock(return_value={"uid": "validuser", "cn": "Valid User"}),
+    )
+
+    # Mock the storage.write async context manager chain:
+    #   storage.write(uid) -> write -> write.as_foundation_committer() -> wafc 
-> wafc.mail.send() -> (mid, [])
+    mock_mail_send = AsyncMock(return_value=("[email protected]", []))
+    mock_wafc = MagicMock()
+    mock_wafc.mail.send = mock_mail_send
+    mock_write = MagicMock()
+    mock_write.as_foundation_committer.return_value = mock_wafc
+
+    @contextlib.asynccontextmanager
+    async def mock_storage_write(_asf_uid: str):  # type: 
ignore[no-untyped-def]
+        yield mock_write
+
+    monkeypatch.setattr("atr.tasks.message.storage.write", mock_storage_write)
+
+    result = await 
message.send(_send_args(email_sender="[email protected]"))
+
+    # Verify the result
+    assert result is not None
+    assert result.mid == "[email protected]"
+    assert result.mail_send_warnings == []
+
+    # Verify mail.send was called exactly once
+    mock_mail_send.assert_called_once()


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to