diff --git a/borg/archiver.py b/borg/archiver.py index 8aff7c2fc..d8f8958e5 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -115,7 +115,7 @@ def print_file_status(self, status, path): def do_serve(self, args): """Start in server mode. This command is usually not used manually. """ - return RepositoryServer(restrict_to_paths=args.restrict_to_paths).serve() + return RepositoryServer(restrict_to_paths=args.restrict_to_paths, append_only=args.append_only).serve() @with_repository(create=True, exclusive=True, manifest=False) def do_init(self, args, repository): @@ -916,6 +916,8 @@ def build_parser(self, args=None, prog=None): subparser.set_defaults(func=self.do_serve) subparser.add_argument('--restrict-to-path', dest='restrict_to_paths', action='append', metavar='PATH', help='restrict repository access to PATH') + subparser.add_argument('--append-only', dest='append_only', action='store_true', + help='only allow appending to repository segment files') init_epilog = textwrap.dedent(""" This command initializes an empty repository. A repository is a filesystem directory containing the deduplicated data from zero or more archives. @@ -1491,8 +1493,9 @@ def get_args(self, argv, cmd): if result.func != forced_result.func: # someone is trying to execute a different borg subcommand, don't do that! return forced_result - # the only thing we take from the forced "borg serve" ssh command is --restrict-to-path + # we only take specific options from the forced "borg serve" command: result.restrict_to_paths = forced_result.restrict_to_paths + result.append_only = forced_result.append_only return result def parse_args(self, args=None): diff --git a/borg/remote.py b/borg/remote.py index e95c38978..85012382a 100644 --- a/borg/remote.py +++ b/borg/remote.py @@ -54,9 +54,10 @@ class RepositoryServer: # pragma: no cover 'break_lock', ) - def __init__(self, restrict_to_paths): + def __init__(self, restrict_to_paths, append_only): self.repository = None self.restrict_to_paths = restrict_to_paths + self.append_only = append_only def serve(self): stdin_fd = sys.stdin.fileno() @@ -123,7 +124,7 @@ def open(self, path, create=False, lock_wait=None, lock=True): break else: raise PathNotAllowed(path) - self.repository = Repository(path, create, lock_wait=lock_wait, lock=lock) + self.repository = Repository(path, create, lock_wait=lock_wait, lock=lock, append_only=self.append_only) self.repository.__enter__() # clean exit handled by serve() method return self.repository.id diff --git a/borg/repository.py b/borg/repository.py index 25f9ccc19..239fd1965 100644 --- a/borg/repository.py +++ b/borg/repository.py @@ -53,7 +53,7 @@ class CheckNeeded(ErrorWithTraceback): class ObjectNotFound(ErrorWithTraceback): """Object with key {} not found in repository {}.""" - def __init__(self, path, create=False, exclusive=False, lock_wait=None, lock=True): + def __init__(self, path, create=False, exclusive=False, lock_wait=None, lock=True, append_only=False): self.path = os.path.abspath(path) self._location = Location('file://%s' % self.path) self.io = None @@ -64,6 +64,7 @@ def __init__(self, path, create=False, exclusive=False, lock_wait=None, lock=Tru self.do_lock = lock self.do_create = create self.exclusive = exclusive + self.append_only = append_only def __del__(self): if self.lock: @@ -169,7 +170,9 @@ def open(self, path, exclusive, lock_wait=None, lock=True): raise self.InvalidRepository(path) self.max_segment_size = self.config.getint('repository', 'max_segment_size') self.segments_per_dir = self.config.getint('repository', 'segments_per_dir') - self.append_only = self.config.getboolean('repository', 'append_only', fallback=False) + # append_only can be set in the constructor + # it shouldn't be overridden (True -> False) here + self.append_only = self.append_only or self.config.getboolean('repository', 'append_only', fallback=False) self.id = unhexlify(self.config.get('repository', 'id').strip()) self.io = LoggedIO(self.path, self.max_segment_size, self.segments_per_dir) diff --git a/borg/testsuite/repository.py b/borg/testsuite/repository.py index 0f384380a..b72e80414 100644 --- a/borg/testsuite/repository.py +++ b/borg/testsuite/repository.py @@ -201,11 +201,14 @@ def test_ignores_commit_tag_in_data(self): class RepositoryAppendOnlyTestCase(RepositoryTestCaseBase): + def open(self, create=False): + return Repository(os.path.join(self.tmppath, 'repository'), create=create, append_only=True) + def test_destroy_append_only(self): # Can't destroy append only repo (via the API) - self.repository.append_only = True with self.assert_raises(ValueError): self.repository.destroy() + assert self.repository.append_only def test_append_only(self): def segments_in_repository(): diff --git a/docs/usage.rst b/docs/usage.rst index b7863d9a9..e1a30060b 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -727,6 +727,13 @@ To activate append-only mode, edit the repository ``config`` file and add a line In append-only mode Borg will create a transaction log in the ``transactions`` file, where each line is a transaction and a UTC timestamp. +In addition, ``borg serve`` can act as if a repository is in append-only mode with +its option ``--append-only``. This can be very useful for fine-tuning access control +in ``.ssh/authorized_keys`` :: + + command="borg serve --append-only ..." ssh-rsa + command="borg serve ..." ssh-rsa + Example +++++++