diff --git a/src/borg/archive.py b/src/borg/archive.py index 3fb95152bd..b317088f90 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -498,6 +498,8 @@ def __init__( deleted=False, ): name_is_id = isinstance(name, bytes) + if not name_is_id: + assert len(name) <= 255 self.cwd = os.getcwd() assert isinstance(manifest, Manifest) self.manifest = manifest diff --git a/src/borg/cache.py b/src/borg/cache.py index 1aeded8aed..621a0e2795 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -7,6 +7,8 @@ from datetime import datetime, timezone, timedelta from time import perf_counter +from borgstore.backends.errors import PermissionDenied + from .logger import create_logger logger = create_logger() @@ -443,7 +445,10 @@ def _build_files_cache(self): from .archive import Archive # get the latest archive with the IDENTICAL name, supporting archive series: - archives = self.manifest.archives.list(match=[self.archive_name], sort_by=["ts"], last=1) + try: + archives = self.manifest.archives.list(match=[self.archive_name], sort_by=["ts"], last=1) + except PermissionDenied: # maybe repo is in write-only mode? + archives = None if not archives: # nothing found return diff --git a/src/borg/manifest.py b/src/borg/manifest.py index 53876cca0d..58029b625d 100644 --- a/src/borg/manifest.py +++ b/src/borg/manifest.py @@ -553,7 +553,6 @@ def write(self): self.timestamp = max_ts.isoformat(timespec="microseconds") # include checks for limits as enforced by limited unpacker (used by load()) assert self.archives.count() <= MAX_ARCHIVES - assert all(len(name) <= 255 for name in self.archives.names()) assert len(self.item_keys) <= 100 self.config["item_keys"] = tuple(sorted(self.item_keys)) manifest_archives = self.archives.finish(self) diff --git a/src/borg/repository.py b/src/borg/repository.py index 8300395e94..3886ed6d11 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -130,11 +130,22 @@ def __init__(self, path_or_location, create=False, exclusive=False, lock_wait=1. "keys": "lr", "locks": "lrwD", # borg needs to create/delete a shared lock here } + elif permissions == "write-only": # mostly no reading + permissions = { + "": "l", + "archives": "lw", + "cache": "lrwWD", # read allowed, e.g. for chunks. cache + "config": "lrW", # W for manifest + "data": "lw", # no r! + "keys": "lr", + "locks": "lrwD", # borg needs to create/delete a shared lock here + } elif permissions == "read-only": # mostly r/o permissions = {"": "lr", "locks": "lrwD"} else: raise Error( - f"Invalid BORG_REPO_PERMISSIONS value: {permissions}, should be one of: all, no-delete, read-only" + f"Invalid BORG_REPO_PERMISSIONS value: {permissions}, should be one of: " + f"all, no-delete, write-only, read-only." ) try: diff --git a/src/borg/testsuite/archiver/restricted_permissions_test.py b/src/borg/testsuite/archiver/restricted_permissions_test.py index bea3ce4474..b3eb4121df 100644 --- a/src/borg/testsuite/archiver/restricted_permissions_test.py +++ b/src/borg/testsuite/archiver/restricted_permissions_test.py @@ -136,3 +136,65 @@ def test_repository_permissions_read_only(archivers, request, monkeypatch): # Try to compact the repo, which should fail. with pytest.raises(PermissionDenied): cmd(archiver, "compact") + + +def test_repository_permissions_write_only(archivers, request, monkeypatch): + """Test repository with 'write-only' permissions setting""" + archiver = request.getfixturevalue(archivers) + + # Create a repository first (need unrestricted permissions for that). + monkeypatch.setenv("BORG_REPO_PERMISSIONS", "all") + cmd(archiver, "repo-create", RK_ENCRYPTION) + + # Create an initial archive to test with. + create_test_files(archiver.input_path) + cmd(archiver, "create", "archive1", "input") + + # Switch to write-only permissions. + monkeypatch.setenv("BORG_REPO_PERMISSIONS", "write-only") + + # Try to create a new archive, which should succeed + cmd(archiver, "create", "archive2", "input") + + # Try to list archives, which should fail (requires reading from data directory). + with pytest.raises(PermissionDenied): + cmd(archiver, "repo-list") + + # Try to list files in an archive, which should fail (requires reading from data directory). + with pytest.raises(PermissionDenied): + cmd(archiver, "list", "archive1") + with pytest.raises(PermissionDenied): + cmd(archiver, "list", "archive2") + + # Try to extract the archive, which should fail (data dir has "lw" permissions, no reading). + with pytest.raises(PermissionDenied): + with changedir("output"): + cmd(archiver, "extract", "archive1") + + # Try to delete an archive, which should fail (requires reading from data directory to identify the archive). + with pytest.raises(PermissionDenied): + cmd(archiver, "delete", "archive1") + + # Try to compact the repo, which should fail (data dir has "lw" permissions, no reading). + with pytest.raises(PermissionDenied): + cmd(archiver, "compact") + + # Try to check the repo, which should fail (data dir has "lw" permissions, no reading). + with pytest.raises(PermissionDenied): + cmd(archiver, "check") + + # Try to delete the repo, which should fail (no "D" permission on data dir). + with pytest.raises(PermissionDenied): + cmd(archiver, "repo-delete") + + # Switch to read-only permissions. + monkeypatch.setenv("BORG_REPO_PERMISSIONS", "read-only") + + # Try to list archives, should work now. + output = cmd(archiver, "repo-list") + assert "archive1" in output + assert "archive2" in output + + # Try to list files in an archive, should work now. + cmd(archiver, "list", "archive1") + cmd(archiver, "list", "archive2")