Browse Source

Merge pull request #8844 from ThomasWaldmann/write-only

repository: add write-only permissions mode, fixes #1165
TW 2 weeks ago
parent
commit
7afb649262

+ 2 - 0
src/borg/archive.py

@@ -498,6 +498,8 @@ class Archive:
         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

+ 6 - 1
src/borg/cache.py

@@ -7,6 +7,8 @@ from collections import namedtuple
 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 @@ class FilesCacheMixin:
         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

+ 0 - 1
src/borg/manifest.py

@@ -553,7 +553,6 @@ class Manifest:
             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)

+ 12 - 1
src/borg/repository.py

@@ -130,11 +130,22 @@ class Repository:
                 "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.<HASH> 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:

+ 62 - 0
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")