فهرست منبع

BORG_REPO_PERMISSIONS=all|no-delete|read-only

The posixfs borgstore backend implements permissions to make
testing with differently permissive stores easier.

The env var selects from pre-defined permission configurations
within borg and gives the chosen permissions config to borgstore.
Thomas Waldmann 3 هفته پیش
والد
کامیت
552646cc9b
2فایلهای تغییر یافته به همراه161 افزوده شده و 1 حذف شده
  1. 23 1
      src/borg/repository.py
  2. 138 0
      src/borg/testsuite/archiver/restricted_permissions_test.py

+ 23 - 1
src/borg/repository.py

@@ -115,8 +115,30 @@ class Repository:
             "keys/": [0],
             "locks/": [0],
         }
+        # Get permissions from environment variable
+        permissions = os.environ.get("BORG_REPO_PERMISSIONS", "all")
+
+        if permissions == "all":
+            permissions = None  # permissions system will not be used
+        elif permissions == "no-delete":  # mostly no delete, no overwrite
+            permissions = {
+                "": "lr",
+                "archives": "lrw",
+                "cache": "lrwWD",  # WD for chunks.X
+                "config": "lrWD",  # W for manifest, D for last-key-checked
+                "data": "lrw",
+                "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"
+            )
+
         try:
-            self.store = Store(url, levels=levels_config)
+            self.store = Store(url, levels=levels_config, permissions=permissions)
         except StoreBackendError as e:
             raise Error(str(e))
         self.store_opened = False

+ 138 - 0
src/borg/testsuite/archiver/restricted_permissions_test.py

@@ -0,0 +1,138 @@
+import os
+import pytest
+
+from borgstore.backends.errors import PermissionDenied
+
+from ...constants import *  # NOQA
+from .. import changedir
+from . import cmd, create_test_files, RK_ENCRYPTION, generate_archiver_tests
+
+pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local")  # NOQA
+
+
+def test_repository_permissions_all(archivers, request, monkeypatch):
+    """Test repository with 'all' permissions setting"""
+    archiver = request.getfixturevalue(archivers)
+
+    # Create a repository with unrestricted permissions.
+    monkeypatch.setenv("BORG_REPO_PERMISSIONS", "all")
+    cmd(archiver, "repo-create", RK_ENCRYPTION)
+
+    create_test_files(archiver.input_path)
+    cmd(archiver, "create", "archive1", "input")
+
+    # Verify the archive was created.
+    assert "archive1" in cmd(archiver, "repo-list")
+
+    # Delete the archive to verify unrestricted permissions.
+    cmd(archiver, "delete", "archive1")
+
+    # Verify the archive was deleted.
+    assert "archive1" not in cmd(archiver, "repo-list")
+
+    # Delete the repository to verify unrestricted permissions.
+    cmd(archiver, "repo-delete")
+
+
+def test_repository_permissions_no_delete(archivers, request, monkeypatch):
+    """Test repository with 'no-delete' permissions setting"""
+    archiver = request.getfixturevalue(archivers)
+    create_test_files(archiver.input_path)
+
+    # Create a repository first (need unrestricted permissions for that).
+    monkeypatch.setenv("BORG_REPO_PERMISSIONS", "all")
+    cmd(archiver, "repo-create", RK_ENCRYPTION)
+    cmd(archiver, "create", "archive1", "input")
+    cmd(archiver, "delete", "archive1")  # this is so that compact has some chunk to remove
+
+    # Switch to no-delete permissions.
+    monkeypatch.setenv("BORG_REPO_PERMISSIONS", "no-delete")
+
+    # Creating new archives should work.
+    cmd(archiver, "create", "archive2", "input")
+
+    # Verify the archive was created.
+    assert "archive2" in cmd(archiver, "repo-list")
+
+    # Try to delete the archive, which should fail.
+    with pytest.raises(PermissionDenied):
+        cmd(archiver, "delete", "archive2")
+
+    # Verify the archive still exists.
+    assert "archive2" in cmd(archiver, "repo-list")
+
+    # Try to rename an archive, which should fail.
+    with pytest.raises(PermissionDenied):
+        cmd(archiver, "rename", "archive2", "archive3")
+
+    # Verify the archive still exists.
+    assert "archive2" in cmd(archiver, "repo-list")
+
+    # Try to delete the repo, which should fail.
+    with pytest.raises(PermissionDenied):
+        cmd(archiver, "repo-delete")
+
+    # Verify the archive still exists.
+    assert "archive2" in cmd(archiver, "repo-list")
+
+    # Try to compact the repo, which should fail.
+    with pytest.raises(PermissionDenied):
+        cmd(archiver, "compact")
+
+    # Check without --repair should work.
+    cmd(archiver, "check")
+
+    # Try to check --repair, which should fail.
+    with pytest.raises(PermissionDenied):
+        cmd(archiver, "check", "--repair")
+
+    # Try to repo-compress (and change compression from lz4 to zstd), which should fail.
+    # It fails because it needs to overwrite existing chunks, which is also disallowed by no-delete.
+    with pytest.raises(PermissionDenied):
+        cmd(archiver, "repo-compress", "-C", "zstd")
+
+
+def test_repository_permissions_read_only(archivers, request, monkeypatch):
+    """Test repository with 'read-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 archive to test with.
+    create_test_files(archiver.input_path)
+    cmd(archiver, "create", "archive2", "input")
+
+    # Switch to read-only permissions.
+    monkeypatch.setenv("BORG_REPO_PERMISSIONS", "read-only")
+
+    # Verify we can list archives.
+    assert "archive2" in cmd(archiver, "repo-list")
+
+    # Verify we can list files in an archive.
+    assert "input/" in cmd(archiver, "list", "archive2")
+
+    # Extract the archive.
+    with changedir("output"):
+        cmd(archiver, "extract", "archive2")
+
+    # Verify extraction worked.
+    extracted_files = os.listdir("output")
+    assert len(extracted_files) > 0
+
+    # Try to create a new archive, which should fail.
+    with pytest.raises(PermissionDenied):
+        cmd(archiver, "create", "archive3", "input")
+
+    # Try to delete an archive, which should fail.
+    with pytest.raises(PermissionDenied):
+        cmd(archiver, "delete", "archive2")
+
+    # Try to delete the repo, which should fail.
+    with pytest.raises(PermissionDenied):
+        cmd(archiver, "repo-delete")
+
+    # Try to compact the repo, which should fail.
+    with pytest.raises(PermissionDenied):
+        cmd(archiver, "compact")