diff --git a/aiosmtpd/docs/NEWS.rst b/aiosmtpd/docs/NEWS.rst index cf7251e0..40d2abff 100644 --- a/aiosmtpd/docs/NEWS.rst +++ b/aiosmtpd/docs/NEWS.rst @@ -9,6 +9,7 @@ Added ----- * Unthreaded Controllers (Closes #160) +* Ability to drop permissions to a user other than ``nobody`` using ``-S`` Fixed/Improved -------------- diff --git a/aiosmtpd/main.py b/aiosmtpd/main.py index 166484ca..cb408fe7 100644 --- a/aiosmtpd/main.py +++ b/aiosmtpd/main.py @@ -48,9 +48,18 @@ def _parser() -> ArgumentParser: default=True, action="store_false", help=( - "This program generally tries to setuid ``nobody``, unless this " - "flag is set. The setuid call will fail if this program is not " - "run as root (in which case, use this flag)." + "This program uses setuid to drop permissions unless this flag " + "is set. The setuid call will fail if this program is not run " + "as root (in which case, use this flag)." + ), + ) + parser.add_argument( + "-S", + "--suid-user", + dest="suid_user", + default="nobody", + help=( + "The user to change to using suid; defaults to ``nobody``." ), ) parser.add_argument( @@ -224,12 +233,13 @@ def main(args: Optional[Sequence[str]] = None) -> None: file=sys.stderr, ) sys.exit(1) - nobody = pwd.getpwnam("nobody").pw_uid + suid_user_id = pwd.getpwnam(args.suid_user).pw_uid try: - os.setuid(nobody) + os.setuid(suid_user_id) except PermissionError: print( - 'Cannot setuid "nobody"; try running with -n option.', file=sys.stderr + f'Cannot setuid to "{args.suid_user}"; try running with -n option.', + file=sys.stderr, ) sys.exit(1) diff --git a/aiosmtpd/tests/test_main.py b/aiosmtpd/tests/test_main.py index e6b38682..f56c3ca8 100644 --- a/aiosmtpd/tests/test_main.py +++ b/aiosmtpd/tests/test_main.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import asyncio +from collections import namedtuple import logging import multiprocessing as MP import os @@ -141,6 +142,17 @@ def test_setuid(self, nobody_uid, mocker): main(args=()) mock.assert_called_with(nobody_uid) + def test_setuid_other(self, nobody_uid, mocker): + other_user = namedtuple( + "pwnam", + ["pw_uid", "pw_dir", "pw_shell"], + )(42, "/", "/bin/sh") + mock_getpwnam = mocker.patch("pwd.getpwnam", return_value=other_user) + mock_suid = mocker.patch("os.setuid") + main(args=("-S", "other")) + mock_getpwnam.assert_called_with("other") + mock_suid.assert_called_with(42) + def test_setuid_permission_error(self, nobody_uid, mocker, capsys): mock = mocker.patch("os.setuid", side_effect=PermissionError) with pytest.raises(SystemExit) as excinfo: @@ -149,7 +161,7 @@ def test_setuid_permission_error(self, nobody_uid, mocker, capsys): mock.assert_called_with(nobody_uid) assert ( capsys.readouterr().err - == 'Cannot setuid "nobody"; try running with -n option.\n' + == 'Cannot setuid to "nobody"; try running with -n option.\n' ) def test_setuid_no_pwd_module(self, nobody_uid, mocker, capsys):