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]