diff --git a/changelog/10738.trivial.rst b/changelog/10738.trivial.rst new file mode 100644 index 00000000000..7c7b668dc31 --- /dev/null +++ b/changelog/10738.trivial.rst @@ -0,0 +1 @@ +Added ``PYTEST_TMPDIR_FILE_MASK`` environment variable which controls the file permissions of any files or directories created by the ``tmp_path`` fixtures. diff --git a/doc/en/how-to/tmp_path.rst b/doc/en/how-to/tmp_path.rst index 792933dd87e..fef8af0d3da 100644 --- a/doc/en/how-to/tmp_path.rst +++ b/doc/en/how-to/tmp_path.rst @@ -155,4 +155,14 @@ When distributing tests on the local machine using ``pytest-xdist``, care is tak automatically configure a basetemp directory for the sub processes such that all temporary data lands below a single per-test run basetemp directory. + +.. _`file permissions`: + +File permissions +---------------- + +Any file or directory created by the above fixtures are by default created with private permissions (file mask 700). + +You can override the file mask by setting the ``PYTEST_TMPDIR_FILE_MASK`` environment variable. + .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 9f9463d8862..06905d959a5 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -51,6 +51,8 @@ 1921, # ERROR_CANT_RESOLVE_FILENAME - fix for broken symlink pointing to itself ) +TMPDIR_FILE_MASK = int(os.getenv("PYTEST_TMPDIR_FILE_MASK", "0o700"), 8) + def _ignore_error(exception): return ( @@ -206,7 +208,7 @@ def _force_symlink( pass -def make_numbered_dir(root: Path, prefix: str, mode: int = 0o700) -> Path: +def make_numbered_dir(root: Path, prefix: str, mode: int = TMPDIR_FILE_MASK) -> Path: """Create a directory with an increased number as suffix for the given prefix.""" for i in range(10): # try up to 10 times to create the folder diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index a9299944dec..ab96575cec9 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -82,6 +82,8 @@ "/var/lib/sss/mc/passwd" ] +TMPDIR_FILE_MASK = int(os.getenv("PYTEST_TMPDIR_FILE_MASK", "0o700"), 8) + def pytest_addoption(parser: Parser) -> None: parser.addoption( @@ -1502,7 +1504,9 @@ def runpytest_subprocess( The result. """ __tracebackhide__ = True - p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700) + p = make_numbered_dir( + root=self.path, prefix="runpytest-", mode=TMPDIR_FILE_MASK + ) args = ("--basetemp=%s" % p,) + args plugins = [x for x in self.plugins if isinstance(x, str)] if plugins: @@ -1521,7 +1525,7 @@ def spawn_pytest( The pexpect child is returned. """ basetemp = self.path / "temp-pexpect" - basetemp.mkdir(mode=0o700) + basetemp.mkdir(mode=TMPDIR_FILE_MASK) invoke = " ".join(map(str, self._getpytestargs())) cmd = f"{invoke} --basetemp={basetemp} {string}" return self.spawn(cmd, expect_timeout=expect_timeout) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index ec44623dc24..7478655d9c5 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -41,6 +41,8 @@ tmppath_result_key = StashKey[Dict[str, bool]]() +TMPDIR_FILE_MASK = int(os.getenv("PYTEST_TMPDIR_FILE_MASK", "0o700"), 8) + @final @dataclasses.dataclass @@ -136,9 +138,11 @@ def mktemp(self, basename: str, numbered: bool = True) -> Path: basename = self._ensure_relative_to_basetemp(basename) if not numbered: p = self.getbasetemp().joinpath(basename) - p.mkdir(mode=0o700) + p.mkdir(mode=TMPDIR_FILE_MASK) else: - p = make_numbered_dir(root=self.getbasetemp(), prefix=basename, mode=0o700) + p = make_numbered_dir( + root=self.getbasetemp(), prefix=basename, mode=TMPDIR_FILE_MASK + ) self._trace("mktemp", p) return p @@ -155,7 +159,7 @@ def getbasetemp(self) -> Path: basetemp = self._given_basetemp if basetemp.exists(): rm_rf(basetemp) - basetemp.mkdir(mode=0o700) + basetemp.mkdir(mode=TMPDIR_FILE_MASK) basetemp = basetemp.resolve() else: from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT") @@ -165,11 +169,11 @@ def getbasetemp(self) -> Path: # make_numbered_dir() call rootdir = temproot.joinpath(f"pytest-of-{user}") try: - rootdir.mkdir(mode=0o700, exist_ok=True) + rootdir.mkdir(mode=TMPDIR_FILE_MASK, exist_ok=True) except OSError: # getuser() likely returned illegal characters for the platform, use unknown back off mechanism rootdir = temproot.joinpath("pytest-of-unknown") - rootdir.mkdir(mode=0o700, exist_ok=True) + rootdir.mkdir(mode=TMPDIR_FILE_MASK, exist_ok=True) # Because we use exist_ok=True with a predictable name, make sure # we are the owners, to prevent any funny business (on unix, where # temproot is usually shared). @@ -197,7 +201,7 @@ def getbasetemp(self) -> Path: root=rootdir, keep=keep, lock_timeout=LOCK_TIMEOUT, - mode=0o700, + mode=TMPDIR_FILE_MASK, ) assert basetemp is not None, basetemp self._basetemp = basetemp diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index fcb0775dd5f..5f48de54545 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -623,3 +623,19 @@ def test_tmp_path_factory_fixes_up_world_readable_permissions( # After - fixed. assert (basetemp.parent.stat().st_mode & 0o077) == 0 + + +def test_tmp_path_factory_user_specified_permissions( + tmp_path: Path, monkeypatch: MonkeyPatch +) -> None: + """Verify that pytest creates directories under /tmp with user specified permissions.""" + # Use the test's tmp_path as the system temproot (/tmp). + monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(tmp_path)) + monkeypatch.setenv("PYTEST_TMPDIR_FILE_MASK", "0o777") + tmp_factory = TempPathFactory(None, 3, "all", lambda *args: None, _ispytest=True) + basetemp = tmp_factory.getbasetemp() + + # User specified permissions. + assert (basetemp.stat().st_mode & 0o000) == 0 + # Parent too (pytest-of-foo). + assert (basetemp.parent.stat().st_mode & 0o000) == 0