Skip to content

Commit 40e04de

Browse files
committed
feat: warn if running as root
1 parent 6a71e68 commit 40e04de

2 files changed

Lines changed: 52 additions & 0 deletions

File tree

invoke/program.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,37 @@ 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+
program.warn_if_running_as_root(is_testing=False)
114+
assert (
115+
sys.stderr.getvalue()
116+
== "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"
117+
)
118+
119+
@trap
120+
def skips_warning_for_exit_false(self):
121+
program = Program()
122+
with patch.object(program, "running_as_root", return_value=True):
123+
program.warn_if_running_as_root(is_testing=True)
124+
assert sys.stderr.getvalue() == ""
125+
126+
@patch("invoke.program.os")
127+
def uses_geteuid_when_available(self, os_):
128+
os_.geteuid.return_value = 0
129+
assert Program().running_as_root() is True
130+
131+
@patch("invoke.program.os", spec=[])
132+
@patch("invoke.program.getpass.getuser")
133+
def falls_back_to_username_when_geteuid_is_missing(
134+
self, getuser
135+
):
136+
getuser.return_value = "root"
137+
assert Program().running_as_root() is True
138+
108139
class normalize_argv:
109140
@patch("invoke.program.sys")
110141
def defaults_to_sys_argv(self, mock_sys):

0 commit comments

Comments
 (0)