This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
The following commit(s) were added to refs/heads/main by this push:
new 8818629 Send email through the storage interface and add audit logging
8818629 is described below
commit 8818629dd0d121656b9b76b71dcbb5dd94335af4
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Jan 22 15:18:28 2026 +0000
Send email through the storage interface and add audit logging
---
atr/server.py | 1 +
atr/storage/__init__.py | 4 ++
atr/storage/writers/__init__.py | 2 +
atr/storage/writers/mail.py | 112 ++++++++++++++++++++++++++++++++++++++++
atr/storage/writers/tokens.py | 12 +----
atr/tasks/message.py | 9 ++--
atr/tasks/vote.py | 12 ++---
7 files changed, 130 insertions(+), 22 deletions(-)
diff --git a/atr/server.py b/atr/server.py
index ad24301..835106c 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -306,6 +306,7 @@ def _app_setup_logging(app: base.QuartApp, config_mode:
config.Mode, app_config:
]
# Output handler: pretty console for dev (Debug and Allow Tests), JSON for
non-dev (Docker, etc.)
+ # TODO: Align this with util.is_dev_environment()?
is_dev = (config_mode == config.Mode.Debug) and app_config.ALLOW_TESTS
output_handler = logging.StreamHandler(sys.stderr)
if is_dev:
diff --git a/atr/storage/__init__.py b/atr/storage/__init__.py
index 3a7305a..49dc769 100644
--- a/atr/storage/__init__.py
+++ b/atr/storage/__init__.py
@@ -139,6 +139,7 @@ class WriteAsGeneralPublic(WriteAs):
self.cache = writers.cache.GeneralPublic(write, self, data)
self.checks = writers.checks.GeneralPublic(write, self, data)
self.keys = writers.keys.GeneralPublic(write, self, data)
+ self.mail = writers.mail.GeneralPublic(write, self, data)
self.policy = writers.policy.GeneralPublic(write, self, data)
self.project = writers.project.GeneralPublic(write, self, data)
self.release = writers.release.GeneralPublic(write, self, data)
@@ -157,6 +158,7 @@ class WriteAsFoundationCommitter(WriteAsGeneralPublic):
self.cache = writers.cache.FoundationCommitter(write, self, data)
self.checks = writers.checks.FoundationCommitter(write, self, data)
self.keys = writers.keys.FoundationCommitter(write, self, data)
+ self.mail = writers.mail.FoundationCommitter(write, self, data)
self.policy = writers.policy.FoundationCommitter(write, self, data)
self.project = writers.project.FoundationCommitter(write, self, data)
self.release = writers.release.FoundationCommitter(write, self, data)
@@ -181,6 +183,7 @@ class
WriteAsCommitteeParticipant(WriteAsFoundationCommitter):
self.cache = writers.cache.CommitteeParticipant(write, self, data,
committee_name)
self.checks = writers.checks.CommitteeParticipant(write, self, data,
committee_name)
self.keys = writers.keys.CommitteeParticipant(write, self, data,
committee_name)
+ self.mail = writers.mail.CommitteeParticipant(write, self, data,
committee_name)
self.policy = writers.policy.CommitteeParticipant(write, self, data,
committee_name)
self.project = writers.project.CommitteeParticipant(write, self, data,
committee_name)
self.release = writers.release.CommitteeParticipant(write, self, data,
committee_name)
@@ -210,6 +213,7 @@ class WriteAsCommitteeMember(WriteAsCommitteeParticipant):
self.checks = writers.checks.CommitteeMember(write, self, data,
committee_name)
self.distributions = writers.distributions.CommitteeMember(write,
self, data, committee_name)
self.keys = writers.keys.CommitteeMember(write, self, data,
committee_name)
+ self.mail = writers.mail.CommitteeMember(write, self, data,
committee_name)
self.policy = writers.policy.CommitteeMember(write, self, data,
committee_name)
self.project = writers.project.CommitteeMember(write, self, data,
committee_name)
self.release = writers.release.CommitteeMember(write, self, data,
committee_name)
diff --git a/atr/storage/writers/__init__.py b/atr/storage/writers/__init__.py
index 27fe59d..9769fd3 100644
--- a/atr/storage/writers/__init__.py
+++ b/atr/storage/writers/__init__.py
@@ -22,6 +22,7 @@ import atr.storage.writers.cache as cache
import atr.storage.writers.checks as checks
import atr.storage.writers.distributions as distributions
import atr.storage.writers.keys as keys
+import atr.storage.writers.mail as mail
import atr.storage.writers.policy as policy
import atr.storage.writers.project as project
import atr.storage.writers.release as release
@@ -38,6 +39,7 @@ __all__ = [
"checks",
"distributions",
"keys",
+ "mail",
"policy",
"project",
"release",
diff --git a/atr/storage/writers/mail.py b/atr/storage/writers/mail.py
new file mode 100644
index 0000000..63ac78b
--- /dev/null
+++ b/atr/storage/writers/mail.py
@@ -0,0 +1,112 @@
+# 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.
+
+from __future__ import annotations
+
+import atr.db as db
+import atr.log as log
+import atr.mail as mail
+import atr.storage as storage
+import atr.util as util
+
+
+class GeneralPublic:
+ def __init__(
+ self,
+ write: storage.Write,
+ write_as: storage.WriteAsGeneralPublic,
+ data: db.Session,
+ ) -> None:
+ self.__write = write
+ self.__write_as = write_as
+ self.__data = data
+
+
+class FoundationCommitter(GeneralPublic):
+ def __init__(
+ self,
+ write: storage.Write,
+ write_as: storage.WriteAsFoundationCommitter,
+ data: db.Session,
+ ) -> None:
+ super().__init__(write, write_as, data)
+ self.__write = write
+ self.__write_as = write_as
+ self.__data = data
+ asf_uid = write.authorisation.asf_uid
+ if asf_uid is None:
+ raise storage.AccessError("No ASF UID")
+ self.__asf_uid = asf_uid
+
+ async def send(self, message: mail.Message) -> tuple[str, list[str]]:
+ is_dev = util.is_dev_environment()
+
+ if is_dev:
+ log.info(f"Dev environment detected, not sending email to
{message.email_recipient}")
+ mid = util.DEV_TEST_MID
+ errors: list[str] = []
+ else:
+ mid, errors = await mail.send(message)
+
+ self.__write_as.append_to_audit_log(
+ sent=not is_dev,
+ email_sender=message.email_sender,
+ email_recipient=message.email_recipient,
+ subject=message.subject,
+ mid=mid,
+ in_reply_to=message.in_reply_to,
+ )
+
+ return mid, errors
+
+
+class CommitteeParticipant(FoundationCommitter):
+ def __init__(
+ self,
+ write: storage.Write,
+ write_as: storage.WriteAsCommitteeParticipant,
+ data: db.Session,
+ committee_name: str,
+ ) -> None:
+ super().__init__(write, write_as, data)
+ self.__write = write
+ self.__write_as = write_as
+ self.__data = data
+ asf_uid = write.authorisation.asf_uid
+ if asf_uid is None:
+ raise storage.AccessError("No ASF UID")
+ self.__asf_uid = asf_uid
+ self.__committee_name = committee_name
+
+
+class CommitteeMember(CommitteeParticipant):
+ def __init__(
+ self,
+ write: storage.Write,
+ write_as: storage.WriteAsCommitteeMember,
+ data: db.Session,
+ committee_name: str,
+ ) -> None:
+ super().__init__(write, write_as, data, committee_name)
+ self.__write = write
+ self.__write_as = write_as
+ self.__data = data
+ asf_uid = write.authorisation.asf_uid
+ if asf_uid is None:
+ raise storage.AccessError("No ASF UID")
+ self.__asf_uid = asf_uid
+ self.__committee_name = committee_name
diff --git a/atr/storage/writers/tokens.py b/atr/storage/writers/tokens.py
index f6b45ed..d7d8001 100644
--- a/atr/storage/writers/tokens.py
+++ b/atr/storage/writers/tokens.py
@@ -26,11 +26,9 @@ import sqlmodel
import atr.db as db
import atr.jwtoken as jwtoken
-import atr.log as log
import atr.mail as mail
import atr.models.sql as sql
import atr.storage as storage
-import atr.util as util
# TODO: Check that this is known and that its emails are correctly discarded
NOREPLY_EMAIL_ADDRESS: Final[str] = "[email protected]"
@@ -81,10 +79,7 @@ class FoundationCommitter(GeneralPublic):
body=f"A new API token called '{label}' was created for your
account. "
"If you did not create this token, please revoke it immediately.",
)
- if util.is_dev_environment():
- log.info("Dev environment detected, pretending to send mail")
- else:
- await mail.send(message)
+ await self.__write_as.mail.send(message)
return pat
async def delete_token(self, token_id: int) -> None:
@@ -109,10 +104,7 @@ class FoundationCommitter(GeneralPublic):
body=f"An API token called '{label}' was deleted from your
account. "
"If you did not delete this token, please check your account
immediately.",
)
- if util.is_dev_environment():
- log.info("Dev environment detected, pretending to send mail")
- else:
- await mail.send(message)
+ await self.__write_as.mail.send(message)
async def issue_jwt(self, pat_text: str) -> str:
pat_hash = hashlib.sha3_256(pat_text.encode()).hexdigest()
diff --git a/atr/tasks/message.py b/atr/tasks/message.py
index 75d9b21..4e59603 100644
--- a/atr/tasks/message.py
+++ b/atr/tasks/message.py
@@ -19,6 +19,7 @@ import atr.log as log
import atr.mail as mail
import atr.models.results as results
import atr.models.schema as schema
+import atr.storage as storage
import atr.tasks.checks as checks
@@ -60,10 +61,10 @@ async def send(args: Send) -> results.Results | None:
in_reply_to=args.in_reply_to,
)
- # Send the email
- # TODO: Move this call into send itself?
- # await mail.set_secret_key_default()
- mid, mail_errors = await mail.send(message)
+ async with storage.write(sender_asf_uid) as write:
+ wafc = write.as_foundation_committer()
+ mid, mail_errors = await wafc.mail.send(message)
+
if mail_errors:
log.warning(f"Mail sending to {args.email_recipient} for subject
'{args.subject}' encountered errors:")
for error in mail_errors:
diff --git a/atr/tasks/vote.py b/atr/tasks/vote.py
index 9c39481..688d4c0 100644
--- a/atr/tasks/vote.py
+++ b/atr/tasks/vote.py
@@ -23,6 +23,7 @@ import atr.log as log
import atr.mail as mail
import atr.models.results as results
import atr.models.schema as schema
+import atr.storage as storage
import atr.tasks.checks as checks
import atr.util as util
@@ -118,14 +119,9 @@ async def _initiate_core_logic(args: Initiate) ->
results.Results | None:
body=body,
)
- if util.is_dev_environment():
- # Pretend to send the mail
- log.info("Dev environment detected, pretending to send mail")
- mid = util.DEV_TEST_MID
- mail_errors = []
- else:
- # Send the mail
- mid, mail_errors = await mail.send(message)
+ async with storage.write(args.initiator_id) as write:
+ wafc = write.as_foundation_committer()
+ mid, mail_errors = await wafc.mail.send(message)
# Original success message structure
result = results.VoteInitiate(
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]