mirror of
https://github.com/borgbackup/borg.git
synced 2026-10-12 02:11:36 +00:00
Merge pull request #10508 from ThomasWaldmann/mount-passcommand
mount: add passcommand mount option, fixes #6804
This commit is contained in:
@@ -630,7 +630,8 @@ class HelpMixIn:
|
||||
passphrase should be initially set when initializing an encrypted repo. Note that the command
|
||||
is executed without a shell. So variables, like ``$HOME`` will work, but ``~`` won't.
|
||||
Mutually exclusive with BORG_PASSPHRASE and BORG_PASSPHRASE_FD, see there.
|
||||
See also BORG_NEW_PASSPHRASE.
|
||||
See also BORG_NEW_PASSPHRASE. For fstab / autofs entries, ``borg mount`` has a
|
||||
``passcommand`` mount option that works like BORG_PASSCOMMAND.
|
||||
BORG_PASSPHRASE_FD (and BORG_NEW_PASSPHRASE_FD, BORG_OTHER_PASSPHRASE_FD)
|
||||
When set, specifies a file descriptor to read a passphrase
|
||||
from. Programs starting borg may choose to open an anonymous pipe
|
||||
|
||||
@@ -14,6 +14,30 @@ from ..logger import create_logger
|
||||
logger = create_logger()
|
||||
|
||||
|
||||
def use_passcommand_mount_option(args):
|
||||
"""Move the passcommand mount option from args.options to BORG_PASSCOMMAND.
|
||||
|
||||
fstab / autofs entries can only give mount options, not environment variables. The key
|
||||
(and thus the passphrase) is loaded before the mount options are processed, so this has
|
||||
to happen before the repository is opened.
|
||||
"""
|
||||
from ..vfs import pop_option
|
||||
|
||||
if not args.options:
|
||||
return
|
||||
options = args.options.split(",")
|
||||
passcommand = pop_option(options, "passcommand", "", None, str)
|
||||
if passcommand is None:
|
||||
return
|
||||
if not passcommand:
|
||||
raise RTError("passcommand mount option: no command given")
|
||||
set_vars = [var for var in ("BORG_PASSPHRASE", "BORG_PASSCOMMAND", "BORG_PASSPHRASE_FD") if var in os.environ]
|
||||
if set_vars:
|
||||
raise RTError(f"The passcommand mount option and {', '.join(set_vars)} are mutually exclusive.")
|
||||
args.options = ",".join(options) or None
|
||||
os.environ["BORG_PASSCOMMAND"] = passcommand
|
||||
|
||||
|
||||
class MountMixIn:
|
||||
def do_mount(self, args):
|
||||
"""Mounts an archive or an entire repository as a FUSE filesystem."""
|
||||
@@ -44,6 +68,7 @@ class MountMixIn:
|
||||
if not os.access(args.mountpoint, os.R_OK | os.W_OK | os.X_OK):
|
||||
raise RTError(f"{args.mountpoint}: Mountpoint must be a **writable** directory")
|
||||
|
||||
use_passcommand_mount_option(args)
|
||||
self._do_mount(args)
|
||||
|
||||
@with_repository()
|
||||
@@ -138,6 +163,15 @@ class MountMixIn:
|
||||
To allow a regular user to use fstab entries, add the ``user`` option:
|
||||
``/path/to/repo /mnt/point fuse.borgfs defaults,noauto,user 0 0``
|
||||
|
||||
fstab / autofs entries can not set environment variables, so for an encrypted
|
||||
repository, use the ``passcommand`` mount option: it works like
|
||||
``BORG_PASSCOMMAND``, e.g. ``passcommand=/usr/local/sbin/borg-pass-backup1``.
|
||||
As mount options are separated by commas, the command can not contain a comma,
|
||||
so better use a script without arguments. ``passcommand`` is mutually exclusive
|
||||
with ``BORG_PASSPHRASE``, ``BORG_PASSCOMMAND`` and ``BORG_PASSPHRASE_FD``.
|
||||
There is no mount option for the passphrase itself, because mount options are
|
||||
visible to other users (e.g. in /etc/fstab and in the ``ps`` output).
|
||||
|
||||
For FUSE configuration and mount options, see the mount.fuse(8) manual page.
|
||||
|
||||
Borg's default behavior is to use the archived user and group names of each
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import errno
|
||||
import os
|
||||
import shlex
|
||||
import stat
|
||||
import sys
|
||||
import time
|
||||
@@ -27,6 +28,7 @@ from ..repository_test import corrupt_chunk_on_disk
|
||||
from . import RK_ENCRYPTION, cmd, assert_dirs_equal, create_regular_file, create_src_archive, open_archive, src_file
|
||||
from . import requires_hardlinks, _extract_hardlinks_setup, fuse_mount, create_test_files, generate_archiver_tests
|
||||
from . import Archiver
|
||||
from ...archiver.mount_cmds import use_passcommand_mount_option
|
||||
|
||||
pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,binary") # NOQA
|
||||
|
||||
@@ -682,3 +684,74 @@ def test_borg_mount_has_no_repository_positional():
|
||||
assert args.mountpoint == "/mnt/point"
|
||||
assert args.paths == ["some/path"]
|
||||
assert not args.location.valid
|
||||
|
||||
|
||||
def print_passphrase_command(passphrase):
|
||||
"""Return a passcommand printing *passphrase* (shlex syntax, run without a shell, no commas)."""
|
||||
python = sys.executable.replace("\\", "/") # see set_empty_passphrase
|
||||
return f"{shlex.quote(python)} -c \"print('{passphrase}')\""
|
||||
|
||||
|
||||
@pytest.mark.skipif(not has_any_fuse, reason="FUSE not available")
|
||||
def test_fuse_passcommand_mount_option(archivers, request, monkeypatch):
|
||||
archiver = request.getfixturevalue(archivers)
|
||||
cmd(archiver, "repo-create", RK_ENCRYPTION)
|
||||
create_src_archive(archiver, "archive")
|
||||
passphrase = os.environ["BORG_PASSPHRASE"]
|
||||
monkeypatch.delenv("BORG_PASSPHRASE")
|
||||
mountpoint = os.path.join(archiver.tmpdir, "mountpoint")
|
||||
with fuse_mount(archiver, mountpoint, "-o", f"passcommand={print_passphrase_command(passphrase)}"):
|
||||
assert os.listdir(mountpoint) == ["archive"]
|
||||
|
||||
|
||||
class PasscommandArgs:
|
||||
"""Minimal stand-in for the parsed borg mount arguments."""
|
||||
|
||||
def __init__(self, options):
|
||||
self.options = options
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def no_passphrase_env(monkeypatch):
|
||||
for var in "BORG_PASSPHRASE", "BORG_PASSCOMMAND", "BORG_PASSPHRASE_FD":
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"options, remaining",
|
||||
[
|
||||
("passcommand=/usr/local/sbin/pass backup1", None),
|
||||
("allow_other,passcommand=/usr/local/sbin/pass backup1,versions", "allow_other,versions"),
|
||||
],
|
||||
)
|
||||
def test_passcommand_mount_option(no_passphrase_env, options, remaining):
|
||||
args = PasscommandArgs(options)
|
||||
use_passcommand_mount_option(args)
|
||||
# it is not a FUSE mount option, so it must not stay in the mount options:
|
||||
assert args.options == remaining
|
||||
assert os.environ["BORG_PASSCOMMAND"] == "/usr/local/sbin/pass backup1"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("options", [None, "", "allow_other,versions"])
|
||||
def test_passcommand_mount_option_not_given(no_passphrase_env, options):
|
||||
args = PasscommandArgs(options)
|
||||
use_passcommand_mount_option(args)
|
||||
assert args.options == options
|
||||
assert "BORG_PASSCOMMAND" not in os.environ
|
||||
|
||||
|
||||
@pytest.mark.parametrize("options", ["passcommand", "passcommand="])
|
||||
def test_passcommand_mount_option_empty(no_passphrase_env, options):
|
||||
with pytest.raises(RTError, match="no command given"):
|
||||
use_passcommand_mount_option(PasscommandArgs(options))
|
||||
assert "BORG_PASSCOMMAND" not in os.environ
|
||||
|
||||
|
||||
@pytest.mark.parametrize("var", ["BORG_PASSPHRASE", "BORG_PASSCOMMAND", "BORG_PASSPHRASE_FD"])
|
||||
def test_passcommand_mount_option_exclusive(no_passphrase_env, monkeypatch, var):
|
||||
# like the passphrase environment variables, the passcommand mount option is mutually exclusive with them.
|
||||
monkeypatch.setenv(var, "0")
|
||||
args = PasscommandArgs("allow_other,passcommand=/usr/local/sbin/pass")
|
||||
with pytest.raises(RTError, match=f"The passcommand mount option and {var} are mutually exclusive"):
|
||||
use_passcommand_mount_option(args)
|
||||
assert os.environ[var] == "0"
|
||||
|
||||
Reference in New Issue
Block a user