Skip to content

Commit e44d8a3

Browse files
authored
Use --non-interactive instead of -A (bugfix) (#2161)
* Use --non-interactive instead of -A * Improve sudo_broker system exit output * Update tests now using check_output Minor: Black * Fix new bug in untested function
1 parent 4226836 commit e44d8a3

File tree

2 files changed

+58
-27
lines changed

2 files changed

+58
-27
lines changed

checkbox-ng/plainbox/impl/secure/sudo_broker.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# This file is part of Checkbox.
22
#
3-
# Copyright 2017-2020 Canonical Ltd.
3+
# Copyright 2017-2025 Canonical Ltd.
44
# Written by:
55
# Maciej Kisielewski <maciej.kisielewski@canonical.com>
6+
# Massimiliano Girardi <massimiliano.girardi@canonical.com>
67
#
78
# Checkbox is free software: you can redistribute it and/or modify
89
# it under the terms of the GNU General Public License version 3,
@@ -30,7 +31,14 @@
3031
import sys
3132

3233
from plainbox.i18n import gettext as _
33-
from subprocess import check_call, CalledProcessError, DEVNULL, SubprocessError
34+
from subprocess import (
35+
check_output,
36+
check_call,
37+
CalledProcessError,
38+
STDOUT,
39+
DEVNULL,
40+
SubprocessError,
41+
)
3442

3543

3644
logger = logging.getLogger("sudo_broker")
@@ -40,27 +48,36 @@ def is_passwordless_sudo():
4048
"""
4149
Check if system can run sudo without pass.
4250
"""
51+
# this command fails if passowrdless sudo is not configured because
52+
# --reset-timestamp will trigger re-authentication
53+
# --non-interactive will make the command fail if user interaction is due
54+
# We no longer use `-A` (ASKPASS) because sudo-rs doesn't currently support
55+
# it
56+
check_passwordless_sudo_cmd = [
57+
"sudo",
58+
"--non-interactive",
59+
"--reset-timestamp",
60+
"true",
61+
]
4362
if os.geteuid() == 0:
4463
# even though we run as root, we still may need to use sudo to switch
4564
# to a normal user for jobs not requiring root, so let's see if sudo
4665
# actually works.
4766
try:
48-
check_call(
49-
["sudo", "-A", "-k", "true"], stdout=DEVNULL, stderr=DEVNULL
67+
check_output(
68+
check_passwordless_sudo_cmd,
69+
stderr=STDOUT,
70+
universal_newlines=True,
5071
)
5172
except (SubprocessError, OSError) as exc:
52-
logger.error(_("Unable to run sudo %s"), exc)
53-
raise SystemExit(1)
73+
try:
74+
print(exc.output)
75+
except AttributeError:
76+
pass
77+
raise SystemExit("Checkbox is unable to run sudo: {}".format(exc))
5478
return True
55-
# running sudo with -A will try using ASKPASS envvar that should specify
56-
# the program to use when asking for password
57-
# If the system is configured to not ask for password, this will silently
58-
# succeed. If the pass is required, it'll return 1 and not ask for pass,
59-
# as the askpass program is not provided
6079
try:
61-
check_call(
62-
["sudo", "-A", "-k", "true"], stdout=DEVNULL, stderr=DEVNULL
63-
)
80+
check_output(check_passwordless_sudo_cmd, stderr=STDOUT)
6481
except CalledProcessError:
6582
return False
6683
return True

checkbox-ng/plainbox/impl/secure/test_sudo_broker.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,46 @@
1717
from subprocess import CalledProcessError
1818
from unittest import TestCase, mock
1919

20-
from plainbox.impl.secure.sudo_broker import is_passwordless_sudo
20+
from plainbox.impl.secure.sudo_broker import (
21+
is_passwordless_sudo,
22+
validate_pass,
23+
)
2124

2225

2326
class IsPasswordlessSudoTests(TestCase):
2427
@mock.patch("os.geteuid", return_value=0)
25-
@mock.patch("plainbox.impl.secure.sudo_broker.check_call")
26-
def test_root_happy(self, mock_check_call, mock_getuid):
27-
mock_check_call.return_value = 0
28+
@mock.patch("plainbox.impl.secure.sudo_broker.check_output")
29+
def test_root_happy(self, mock_check_output, mock_getuid):
30+
mock_check_output.return_value = 0
2831
self.assertTrue(is_passwordless_sudo())
2932

3033
@mock.patch("os.geteuid", return_value=0)
31-
@mock.patch("plainbox.impl.secure.sudo_broker.check_call")
32-
def test_root_raising(self, mock_check_call, mock_getuid):
33-
mock_check_call.side_effect = OSError
34+
@mock.patch("plainbox.impl.secure.sudo_broker.check_output")
35+
def test_root_raising(self, mock_check_output, mock_getuid):
36+
mock_check_output.side_effect = OSError
3437

3538
with self.assertRaises(SystemExit):
3639
is_passwordless_sudo()
3740

3841
@mock.patch("os.geteuid", return_value=1000)
39-
@mock.patch("plainbox.impl.secure.sudo_broker.check_call")
40-
def test_non_root_happy(self, mock_check_call, mock_getuid):
41-
mock_check_call.return_value = 0
42+
@mock.patch("plainbox.impl.secure.sudo_broker.check_output")
43+
def test_non_root_happy(self, mock_check_output, mock_getuid):
44+
mock_check_output.return_value = 0
4245
self.assertTrue(is_passwordless_sudo())
4346

4447
@mock.patch("os.geteuid", return_value=1000)
45-
@mock.patch("plainbox.impl.secure.sudo_broker.check_call")
46-
def test_non_root_raising(self, mock_check_call, mock_getuid):
47-
mock_check_call.side_effect = CalledProcessError(1, "oops")
48+
@mock.patch("plainbox.impl.secure.sudo_broker.check_output")
49+
def test_non_root_raising(self, mock_check_output, mock_getuid):
50+
mock_check_output.side_effect = CalledProcessError(1, "oops")
4851
self.assertFalse(is_passwordless_sudo())
52+
53+
54+
class ValidatePassTest(TestCase):
55+
@mock.patch("plainbox.impl.secure.sudo_broker.check_call")
56+
def test_validate_pass_wrong(self, check_output_mock):
57+
check_output_mock.side_effect = CalledProcessError(1, "oops")
58+
self.assertFalse(validate_pass(b"password"))
59+
60+
@mock.patch("plainbox.impl.secure.sudo_broker.check_call")
61+
def test_validate_pass_ok(self, check_output_mock):
62+
self.assertTrue(validate_pass(b"password"))

0 commit comments

Comments
 (0)