From 15ddeec138349f0b8233a4b2ece2d16cb30ba874 Mon Sep 17 00:00:00 2001 From: Maciej Urbanski Date: Thu, 15 Aug 2024 14:46:34 +0200 Subject: [PATCH 1/3] fix scan dir permission errors --- b2sdk/_internal/scan/folder.py | 18 ++++- changelog.d/+scan_perm_errors.fixed.md | 2 + test/unit/conftest.py | 26 ++++++++ test/unit/scan/test_folder_traversal.py | 89 ++++++++++++++++++++----- 4 files changed, 117 insertions(+), 18 deletions(-) create mode 100644 changelog.d/+scan_perm_errors.fixed.md diff --git a/b2sdk/_internal/scan/folder.py b/b2sdk/_internal/scan/folder.py index e5ac283e..339f941e 100644 --- a/b2sdk/_internal/scan/folder.py +++ b/b2sdk/_internal/scan/folder.py @@ -236,7 +236,14 @@ def _walk_relative_paths( return # Skip if symlink already visited visited_symlinks.add(inode_number) - for local_path in sorted(local_dir.iterdir()): + try: + dir_children = sorted(local_dir.iterdir()) + except PermissionError: # `chmod -r dir` can trigger this + if reporter is not None: + reporter.local_permission_error(str(local_dir)) + return + + for local_path in dir_children: name = local_path.name relative_file_path = join_b2_path(relative_dir_path, name) @@ -251,7 +258,14 @@ def _walk_relative_paths( reporter.invalid_name(str(local_path), str(e)) continue - if local_path.is_dir(): + try: + is_dir = local_path.is_dir() + except PermissionError: # `chmod -x dir` can trigger this + if reporter is not None: + reporter.local_permission_error(str(local_path)) + continue + + if is_dir: if policies_manager.should_exclude_local_directory(str(relative_file_path)): continue # Skip excluded directories # Recurse into directories diff --git a/changelog.d/+scan_perm_errors.fixed.md b/changelog.d/+scan_perm_errors.fixed.md new file mode 100644 index 00000000..7f6303fe --- /dev/null +++ b/changelog.d/+scan_perm_errors.fixed.md @@ -0,0 +1,2 @@ +Fix LocalFolder.all_files(..) erroring out if one of the non-excluded directories is not readable by the user running the scan. +Warning is added to ProgressReport instead as other file access errors are. diff --git a/test/unit/conftest.py b/test/unit/conftest.py index fdfdadd0..caa04e36 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -10,6 +10,7 @@ from __future__ import annotations import os +import shutil import sys from glob import glob from pathlib import Path @@ -192,3 +193,28 @@ def bucket(b2api): @pytest.fixture def file_info(): return {'key': 'value'} + + +@pytest.fixture +def tmp_path_permission_cleanup(tmp_path): + """ + Ensure tmp_path is delete-able after the test. + + Important for the tests that mess with filesystem permissions. + """ + yield + + try: + shutil.rmtree(tmp_path) + except OSError: + tmp_path.chmod(0o700) + for root, dirs, files in tmp_path.walk(top_down=True): + for name in dirs: + (root / name).chmod(0o700) + for name in files: + (root / name).chmod(0o600) + (root / name).unlink() + for root, dirs, files in tmp_path.walk(top_down=False): + for name in dirs: + (root / name).rmdir() + tmp_path.rmdir() diff --git a/test/unit/scan/test_folder_traversal.py b/test/unit/scan/test_folder_traversal.py index d791dcd0..7b5a9874 100644 --- a/test/unit/scan/test_folder_traversal.py +++ b/test/unit/scan/test_folder_traversal.py @@ -682,23 +682,85 @@ def test_excluded_no_access_check(self, tmp_path): reporter.close() - def test_excluded_without_permissions(self, tmp_path): + @pytest.mark.skipif( + platform.system() == 'Windows', + reason="Unix-only filesystem permissions are tested", + ) + def test_dir_without_exec_permission(self, tmp_path, tmp_path_permission_cleanup): + """Test that a excluded directory/file without permissions emits warnings.""" + no_perm_dir = tmp_path / "no_perm_dir" + no_perm_dir.mkdir() + (no_perm_dir / "file.txt").touch() + (no_perm_dir / "file2.txt").touch() + # chmod -x no_perm_dir + no_perm_dir.chmod(0o600) + + scan_policy = ScanPoliciesManager() + reporter = ProgressReport(sys.stdout, False) + + folder = LocalFolder(str(tmp_path)) + local_paths = folder.all_files(reporter=reporter, policies_manager=scan_policy) + absolute_paths = [path.absolute_path for path in local_paths] + assert not absolute_paths + + # Check that no access warnings are issued for the excluded directory/file + assert set(reporter.warnings) == { + f'WARNING: {tmp_path}/no_perm_dir/file.txt could not be accessed (no permissions to read?)', + f'WARNING: {tmp_path}/no_perm_dir/file2.txt could not be accessed (no permissions to read?)', + } + + reporter.close() + + def test_without_permissions(self, tmp_path, tmp_path_permission_cleanup): + """Test that a excluded directory/file without permissions emits warnings.""" + no_perm_dir = tmp_path / "no_perm_dir" + no_perm_dir.mkdir() + (no_perm_dir / "file.txt").touch() + + included_dir = tmp_path / "included_dir" + included_dir.mkdir() + (included_dir / "no_perm_file.txt").touch() + (included_dir / "included_file.txt").touch() + + # Modify directory permissions to simulate lack of access + (included_dir / "no_perm_file.txt").chmod(0o000) + no_perm_dir.chmod(0o000) + + scan_policy = ScanPoliciesManager() + reporter = ProgressReport(sys.stdout, False) + + folder = LocalFolder(str(tmp_path)) + local_paths = folder.all_files(reporter=reporter, policies_manager=scan_policy) + absolute_paths = [path.absolute_path for path in local_paths] + + # Check that only included_dir/included_file.txt was return + assert any('included_file.txt' in path for path in absolute_paths) + + # Check that no access warnings are issued for the excluded directory/file + assert set(reporter.warnings) == { + f'WARNING: {tmp_path}/no_perm_dir could not be accessed (no permissions to read?)', + f'WARNING: {tmp_path}/included_dir/no_perm_file.txt could not be accessed (no permissions to read?)' + } + + reporter.close() + + def test_excluded_without_permissions(self, tmp_path, tmp_path_permission_cleanup): """Test that a excluded directory/file without permissions is not processed and no warning is issued.""" - excluded_dir = tmp_path / "excluded_dir" - excluded_dir.mkdir() - (excluded_dir / "file.txt").touch() + no_perm_dir = tmp_path / "no_perm_dir" + no_perm_dir.mkdir() + (no_perm_dir / "file.txt").touch() included_dir = tmp_path / "included_dir" included_dir.mkdir() - (included_dir / "excluded_file.txt").touch() + (included_dir / "no_perm_file.txt").touch() (included_dir / "included_file.txt").touch() # Modify directory permissions to simulate lack of access - (included_dir / "excluded_file.txt").chmod(0o000) - excluded_dir.chmod(0o000) + (included_dir / "no_perm_file.txt").chmod(0o000) + no_perm_dir.chmod(0o000) scan_policy = ScanPoliciesManager( - exclude_dir_regexes=[r"excluded_dir$"], exclude_file_regexes=[r'.*excluded_file.txt'] + exclude_dir_regexes=[r"no_perm_dir$"], exclude_file_regexes=[r'.*no_perm_file.txt'] ) reporter = ProgressReport(sys.stdout, False) @@ -707,18 +769,13 @@ def test_excluded_without_permissions(self, tmp_path): absolute_paths = [path.absolute_path for path in local_paths] # Restore directory permissions to clean up - (included_dir / "excluded_file.txt").chmod(0o755) - excluded_dir.chmod(0o755) + (included_dir / "no_perm_file.txt").chmod(0o755) + no_perm_dir.chmod(0o755) # Check that only included_dir/included_file.txt was return assert any('included_file.txt' in path for path in absolute_paths) # Check that no access warnings are issued for the excluded directory/file - assert not any( - re.match( - r"WARNING: .*excluded_.* could not be accessed \(no permissions to read\?\)", - warning, - ) for warning in reporter.warnings - ), "Access warning was issued for the excluded directory/file" + assert not reporter.warnings reporter.close() From 72344a144ca777a356347d2943906d9eecaf99d5 Mon Sep 17 00:00:00 2001 From: Maciej Urbanski Date: Thu, 15 Aug 2024 15:14:35 +0200 Subject: [PATCH 2/3] fix old python compatiblity in `tmp_path_permission_cleanup` fixture --- test/unit/conftest.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/test/unit/conftest.py b/test/unit/conftest.py index caa04e36..3ac861b2 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -208,13 +208,17 @@ def tmp_path_permission_cleanup(tmp_path): shutil.rmtree(tmp_path) except OSError: tmp_path.chmod(0o700) - for root, dirs, files in tmp_path.walk(top_down=True): + + for root, dirs, files in os.walk(tmp_path, topdown=True): for name in dirs: - (root / name).chmod(0o700) + (Path(root) / name).chmod(0o700) for name in files: - (root / name).chmod(0o600) - (root / name).unlink() - for root, dirs, files in tmp_path.walk(top_down=False): + file_path = Path(root) / name + file_path.chmod(0o600) + file_path.unlink() + + for root, dirs, files in os.walk(tmp_path, topdown=False): for name in dirs: - (root / name).rmdir() + (Path(root) / name).rmdir() + tmp_path.rmdir() From b8e8e5d271d4a2959cda36724bad3ead855ac788 Mon Sep 17 00:00:00 2001 From: Maciej Urbanski Date: Thu, 15 Aug 2024 16:13:16 +0200 Subject: [PATCH 3/3] fix fs permission under windows --- b2sdk/_internal/scan/folder.py | 20 ++++++- b2sdk/_internal/scan/report.py | 24 ++++++-- pdm.lock | 25 ++++++++- pyproject.toml | 3 +- test/unit/conftest.py | 75 +++++++++++++++++++++++-- test/unit/scan/test_folder_traversal.py | 36 ++++++------ 6 files changed, 152 insertions(+), 31 deletions(-) diff --git a/b2sdk/_internal/scan/folder.py b/b2sdk/_internal/scan/folder.py index 339f941e..d5a6e578 100644 --- a/b2sdk/_internal/scan/folder.py +++ b/b2sdk/_internal/scan/folder.py @@ -108,6 +108,20 @@ def join_b2_path(relative_dir_path: str | Path, file_name: str): return relative_dir_path + '/' + file_name +if sys.platform == 'win32': + + def _file_read_access(path): + try: + with open(path, 'rb', buffering=0): + return True + except PermissionError: + return False +else: + + def _file_read_access(path): + return os.access(path, os.R_OK) + + class LocalFolder(AbstractFolder): """ Folder interface to a directory on the local machine. @@ -261,7 +275,9 @@ def _walk_relative_paths( try: is_dir = local_path.is_dir() except PermissionError: # `chmod -x dir` can trigger this - if reporter is not None: + if reporter is not None and not policies_manager.should_exclude_local_directory( + str(relative_file_path) + ): reporter.local_permission_error(str(local_path)) continue @@ -292,7 +308,7 @@ def _walk_relative_paths( if policies_manager.should_exclude_local_path(local_scan_path): continue # Skip excluded files - if not os.access(local_path, os.R_OK): + if not _file_read_access(local_path): if reporter is not None: reporter.local_permission_error(str(local_path)) continue diff --git a/b2sdk/_internal/scan/report.py b/b2sdk/_internal/scan/report.py index bcd24448..1a408f21 100644 --- a/b2sdk/_internal/scan/report.py +++ b/b2sdk/_internal/scan/report.py @@ -10,6 +10,7 @@ from __future__ import annotations import logging +import re import threading import time from dataclasses import dataclass @@ -20,6 +21,21 @@ logger = logging.getLogger(__name__) +_REMOVE_EXTENDED_PATH_PREFIX = re.compile(r'\\\\\?\\') + + +def _safe_path_print(path: str) -> str: + """ + Print a path, escaping control characters if necessary. + + Windows extended path prefix is removed from the path before printing for better readability. + Since Windows 10 the prefix is not needed. + + :param path: a path to print + :return: a path that can be printed + """ + return escape_control_chars(_REMOVE_EXTENDED_PATH_PREFIX.sub('', path)) + @dataclass class ProgressReport: @@ -180,7 +196,7 @@ def local_access_error(self, path: str) -> None: :param path: file path """ self.warnings.append( - f'WARNING: {escape_control_chars(path)} could not be accessed (broken symlink?)' + f'WARNING: {_safe_path_print(path)} could not be accessed (broken symlink?)' ) def local_permission_error(self, path: str) -> None: @@ -190,7 +206,7 @@ def local_permission_error(self, path: str) -> None: :param path: file path """ self.warnings.append( - f'WARNING: {escape_control_chars(path)} could not be accessed (no permissions to read?)' + f'WARNING: {_safe_path_print(path)} could not be accessed (no permissions to read?)' ) def symlink_skipped(self, path: str) -> None: @@ -203,7 +219,7 @@ def circular_symlink_skipped(self, path: str) -> None: :param path: file path """ self.warnings.append( - f'WARNING: {escape_control_chars(path)} is a circular symlink, which was already visited. Skipping.' + f'WARNING: {_safe_path_print(path)} is a circular symlink, which was already visited. Skipping.' ) def invalid_name(self, path: str, error: str) -> None: @@ -213,7 +229,7 @@ def invalid_name(self, path: str, error: str) -> None: :param path: file path """ self.warnings.append( - f'WARNING: {escape_control_chars(path)} path contains invalid name ({error}). Skipping.' + f'WARNING: {_safe_path_print(path)} path contains invalid name ({error}). Skipping.' ) diff --git a/pdm.lock b/pdm.lock index fea1fe23..c831b06b 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "doc", "format", "lint", "release", "test"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.4.1" -content_hash = "sha256:55a756ba803efb2577e9990692b874473779c14231d658ca48e9bb7b56fab153" +content_hash = "sha256:6258c29aaf5071b61d83ac4b33e494033e45a95a3a0fadef0676f8e0ad31555a" [[package]] name = "alabaster" @@ -925,6 +925,29 @@ files = [ {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, ] +[[package]] +name = "pywin32" +version = "306" +summary = "Python for Window Extensions" +groups = ["test"] +marker = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\"" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + [[package]] name = "requests" version = "2.31.0" diff --git a/pyproject.toml b/pyproject.toml index 27455176..eb7c74b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,8 +30,6 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] -[project.optional-dependencies] - [project.urls] Homepage = "https://github.com/Backblaze/b2-sdk-python" @@ -174,6 +172,7 @@ test = [ # remove `and platform_python_implementation!='PyPy'` after dropping Python 3.7 support as that # will allow us to update pydantic to a version which installs properly under PyPy "pydantic>=2.0.1,<3; python_version>='3.8' and platform_python_implementation!='PyPy'", + "pywin32>=306; sys_platform == \"win32\" and platform_python_implementation!='PyPy'", ] release = [ "towncrier==23.11.0; python_version>='3.8'", diff --git a/test/unit/conftest.py b/test/unit/conftest.py index 3ac861b2..15929bae 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -15,6 +15,13 @@ from glob import glob from pathlib import Path +try: + import ntsecuritycon + import win32api + import win32security +except ImportError: + ntsecuritycon = win32api = win32security = None + import pytest pytest.register_assert_rewrite('test.unit') @@ -195,26 +202,84 @@ def file_info(): return {'key': 'value'} +class PermTool: + def allow_access(self, path): + pass + + def deny_access(self, path): + pass + + +class UnixPermTool(PermTool): + def allow_access(self, path): + path.chmod(0o700) + + def deny_access(self, path): + path.chmod(0o000) + + +class WindowsPermTool(PermTool): + def __init__(self): + self.user_sid = win32security.GetTokenInformation( + win32security.OpenProcessToken(win32api.GetCurrentProcess(), win32security.TOKEN_QUERY), + win32security.TokenUser + )[0] + + def allow_access(self, path): + dacl = win32security.ACL() + dacl.AddAccessAllowedAce( + win32security.ACL_REVISION, ntsecuritycon.FILE_ALL_ACCESS, self.user_sid + ) + + security_desc = win32security.GetFileSecurity( + str(path), win32security.DACL_SECURITY_INFORMATION + ) + security_desc.SetSecurityDescriptorDacl(1, dacl, 0) + win32security.SetFileSecurity( + str(path), win32security.DACL_SECURITY_INFORMATION, security_desc + ) + + def deny_access(self, path): + dacl = win32security.ACL() + dacl.AddAccessDeniedAce( + win32security.ACL_REVISION, ntsecuritycon.FILE_ALL_ACCESS, self.user_sid + ) + + security_desc = win32security.GetFileSecurity( + str(path), win32security.DACL_SECURITY_INFORMATION + ) + security_desc.SetSecurityDescriptorDacl(1, dacl, 0) + win32security.SetFileSecurity( + str(path), win32security.DACL_SECURITY_INFORMATION, security_desc + ) + + @pytest.fixture -def tmp_path_permission_cleanup(tmp_path): +def fs_perm_tool(tmp_path): """ Ensure tmp_path is delete-able after the test. Important for the tests that mess with filesystem permissions. """ - yield + if os.name == 'nt': + if win32api is None: + pytest.skip('pywin32 is required to run this test') + perm_tool = WindowsPermTool() + else: + perm_tool = UnixPermTool() + yield perm_tool try: shutil.rmtree(tmp_path) except OSError: - tmp_path.chmod(0o700) + perm_tool.allow_access(tmp_path) for root, dirs, files in os.walk(tmp_path, topdown=True): for name in dirs: - (Path(root) / name).chmod(0o700) + perm_tool.allow_access(Path(root) / name) for name in files: file_path = Path(root) / name - file_path.chmod(0o600) + perm_tool.allow_access(file_path) file_path.unlink() for root, dirs, files in os.walk(tmp_path, topdown=False): diff --git a/test/unit/scan/test_folder_traversal.py b/test/unit/scan/test_folder_traversal.py index 7b5a9874..319f214f 100644 --- a/test/unit/scan/test_folder_traversal.py +++ b/test/unit/scan/test_folder_traversal.py @@ -11,8 +11,10 @@ import codecs import os +import pathlib import platform import re +import shlex import sys from unittest.mock import MagicMock, patch @@ -686,7 +688,7 @@ def test_excluded_no_access_check(self, tmp_path): platform.system() == 'Windows', reason="Unix-only filesystem permissions are tested", ) - def test_dir_without_exec_permission(self, tmp_path, tmp_path_permission_cleanup): + def test_dir_without_exec_permission(self, tmp_path, fs_perm_tool): """Test that a excluded directory/file without permissions emits warnings.""" no_perm_dir = tmp_path / "no_perm_dir" no_perm_dir.mkdir() @@ -705,13 +707,13 @@ def test_dir_without_exec_permission(self, tmp_path, tmp_path_permission_cleanup # Check that no access warnings are issued for the excluded directory/file assert set(reporter.warnings) == { - f'WARNING: {tmp_path}/no_perm_dir/file.txt could not be accessed (no permissions to read?)', - f'WARNING: {tmp_path}/no_perm_dir/file2.txt could not be accessed (no permissions to read?)', + f'WARNING: {tmp_path/"no_perm_dir/file.txt"} could not be accessed (no permissions to read?)', + f'WARNING: {tmp_path/"no_perm_dir/file2.txt"} could not be accessed (no permissions to read?)', } reporter.close() - def test_without_permissions(self, tmp_path, tmp_path_permission_cleanup): + def test_without_permissions(self, tmp_path, fs_perm_tool): """Test that a excluded directory/file without permissions emits warnings.""" no_perm_dir = tmp_path / "no_perm_dir" no_perm_dir.mkdir() @@ -723,28 +725,32 @@ def test_without_permissions(self, tmp_path, tmp_path_permission_cleanup): (included_dir / "included_file.txt").touch() # Modify directory permissions to simulate lack of access - (included_dir / "no_perm_file.txt").chmod(0o000) - no_perm_dir.chmod(0o000) + fs_perm_tool.deny_access(included_dir / "no_perm_file.txt") + fs_perm_tool.deny_access(no_perm_dir) scan_policy = ScanPoliciesManager() reporter = ProgressReport(sys.stdout, False) folder = LocalFolder(str(tmp_path)) local_paths = folder.all_files(reporter=reporter, policies_manager=scan_policy) - absolute_paths = [path.absolute_path for path in local_paths] + absolute_paths = [pathlib.Path(path.absolute_path) for path in local_paths] # Check that only included_dir/included_file.txt was return - assert any('included_file.txt' in path for path in absolute_paths) + assert {path.name for path in absolute_paths} == {"included_file.txt"} + + def s(p): + # shlex.quote works differently depending if its on windows or unix + return shlex.quote(str(p)) # Check that no access warnings are issued for the excluded directory/file assert set(reporter.warnings) == { - f'WARNING: {tmp_path}/no_perm_dir could not be accessed (no permissions to read?)', - f'WARNING: {tmp_path}/included_dir/no_perm_file.txt could not be accessed (no permissions to read?)' + f'WARNING: {s(tmp_path / "no_perm_dir")} could not be accessed (no permissions to read?)', + f'WARNING: {s(tmp_path / "included_dir/no_perm_file.txt")} could not be accessed (no permissions to read?)' } reporter.close() - def test_excluded_without_permissions(self, tmp_path, tmp_path_permission_cleanup): + def test_excluded_without_permissions(self, tmp_path, fs_perm_tool): """Test that a excluded directory/file without permissions is not processed and no warning is issued.""" no_perm_dir = tmp_path / "no_perm_dir" no_perm_dir.mkdir() @@ -756,8 +762,8 @@ def test_excluded_without_permissions(self, tmp_path, tmp_path_permission_cleanu (included_dir / "included_file.txt").touch() # Modify directory permissions to simulate lack of access - (included_dir / "no_perm_file.txt").chmod(0o000) - no_perm_dir.chmod(0o000) + fs_perm_tool.deny_access(included_dir / "no_perm_file.txt") + fs_perm_tool.deny_access(no_perm_dir) scan_policy = ScanPoliciesManager( exclude_dir_regexes=[r"no_perm_dir$"], exclude_file_regexes=[r'.*no_perm_file.txt'] @@ -768,10 +774,6 @@ def test_excluded_without_permissions(self, tmp_path, tmp_path_permission_cleanu local_paths = folder.all_files(reporter=reporter, policies_manager=scan_policy) absolute_paths = [path.absolute_path for path in local_paths] - # Restore directory permissions to clean up - (included_dir / "no_perm_file.txt").chmod(0o755) - no_perm_dir.chmod(0o755) - # Check that only included_dir/included_file.txt was return assert any('included_file.txt' in path for path in absolute_paths)