Skip to content

Commit 5d8812b

Browse files
committed
feat: warn if running as root
1 parent 6a71e68 commit 5d8812b

2 files changed

Lines changed: 63 additions & 1 deletion

File tree

invoke/program.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from .exceptions import CollectionNotFound, Exit, ParseError, UnexpectedExit
2222
from .parser import Argument, Parser, ParserContext
2323
from .terminals import pty_size
24-
from .util import debug, enable_logging, helpline
24+
from .util import debug, enable_logging, helpline, isatty
2525

2626
if TYPE_CHECKING:
2727
from .loader import Loader
@@ -186,6 +186,10 @@ def task_args(self) -> List["Argument"]:
186186
indent_width = 4
187187
indent = " " * indent_width
188188
col_padding = 3
189+
root_warning = (
190+
"WARNING: Running Invoke as root may create root-owned files and "
191+
"cause later I/O or permission errors. Re-run as a non-root user."
192+
)
189193

190194
def __init__(
191195
self,
@@ -373,6 +377,7 @@ def run(self, argv: Optional[List[str]] = None, exit: bool = True) -> None:
373377
.. versionadded:: 1.0
374378
"""
375379
try:
380+
self.warn_if_running_as_root(is_testing=not exit)
376381
# Create an initial config, which will hold defaults & values from
377382
# most config file locations (all but runtime.) Used to inform
378383
# loading & parsing behavior.
@@ -421,6 +426,22 @@ def run(self, argv: Optional[List[str]] = None, exit: bool = True) -> None:
421426
except KeyboardInterrupt:
422427
sys.exit(1) # Same behavior as Python itself outside of REPL
423428

429+
def warn_if_running_as_root(self, is_testing: bool = False) -> None:
430+
"""
431+
Emit a warning when Invoke is executed as the root user.
432+
"""
433+
if is_testing or not isatty(sys.stderr) or not self.running_as_root():
434+
return
435+
print(self.root_warning, file=sys.stderr)
436+
437+
def running_as_root(self) -> bool:
438+
"""
439+
Return ``True`` when the current process is running as root.
440+
"""
441+
if hasattr(os, "geteuid"):
442+
return os.geteuid() == 0
443+
return getpass.getuser() == "root"
444+
424445
def parse_core(self, argv: Optional[List[str]]) -> None:
425446
debug("argv given to Program.run: {!r}".format(argv))
426447
self.normalize_argv(argv)

tests/program.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,47 @@ def write_pyc_explicitly_enables_bytecode_writing(self):
105105
expect("--write-pyc -c foo mytask")
106106
assert not sys.dont_write_bytecode
107107

108+
class root_warning:
109+
@trap
110+
def prints_warning_to_tty_stderr(self):
111+
program = Program()
112+
with patch.object(program, "running_as_root", return_value=True):
113+
sys.stderr.isatty = Mock(return_value=True)
114+
program.warn_if_running_as_root(is_testing=False)
115+
assert (
116+
sys.stderr.getvalue()
117+
== "WARNING: Running Invoke as root may create root-owned files and cause later I/O or permission errors. Re-run as a non-root user.\n"
118+
)
119+
120+
@trap
121+
def does_not_warn_when_stderr_is_not_a_tty(self):
122+
program = Program()
123+
with patch.object(program, "running_as_root", return_value=True):
124+
sys.stderr.isatty = Mock(return_value=False)
125+
program.warn_if_running_as_root(is_testing=False)
126+
assert sys.stderr.getvalue() == ""
127+
128+
@trap
129+
def skips_warning_for_exit_false(self):
130+
program = Program()
131+
with patch.object(program, "running_as_root", return_value=True):
132+
sys.stderr.isatty = Mock(return_value=True)
133+
program.warn_if_running_as_root(is_testing=True)
134+
assert sys.stderr.getvalue() == ""
135+
136+
@patch("invoke.program.os")
137+
def uses_geteuid_when_available(self, os_):
138+
os_.geteuid.return_value = 0
139+
assert Program().running_as_root() is True
140+
141+
@patch("invoke.program.os", spec=[])
142+
@patch("invoke.program.getpass.getuser")
143+
def falls_back_to_username_when_geteuid_is_missing(
144+
self, getuser
145+
):
146+
getuser.return_value = "root"
147+
assert Program().running_as_root() is True
148+
108149
class normalize_argv:
109150
@patch("invoke.program.sys")
110151
def defaults_to_sys_argv(self, mock_sys):

0 commit comments

Comments
 (0)