diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 2a688f8b3..d2921c7a5 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -223,7 +223,7 @@ since the quota can be changed in the repository config. The quota is enforcible only if *all* :ref:`borg_serve` versions accessible to clients support quotas (see next section). Further, quota is per repository. Therefore, ensure clients can only access a defined set of repositories -with their quotas set, using ``--restrict-to-path``. +with their quotas set, using ``--restrict-to-repository``. If the client exceeds the storage quota the ``StorageQuotaExceeded`` exception is raised. Normally a client could ignore such an exception and just send a ``commit()`` diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 2081b44ec..77fdbf14b 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -215,6 +215,7 @@ class Archiver: """Start in server mode. This command is usually not used manually.""" return RepositoryServer( restrict_to_paths=args.restrict_to_paths, + restrict_to_repositories=args.restrict_to_repositories, append_only=args.append_only, storage_quota=args.storage_quota, ).serve() @@ -2339,6 +2340,14 @@ class Archiver: metavar='PATH', help='restrict repository access to PATH. ' 'Can be specified multiple times to allow the client access to several directories. ' 'Access to all sub-directories is granted implicitly; PATH doesn\'t need to directly point to a repository.') + subparser.add_argument('--restrict-to-repository', dest='restrict_to_repositories', action='append', + metavar='PATH', help='restrict repository access. Only the repository located at PATH (no sub-directories are considered) ' + 'is accessible. ' + 'Can be specified multiple times to allow the client access to several repositories. ' + 'Unlike --restrict-to-path sub-directories are not accessible; ' + 'PATH needs to directly point at a repository location. ' + 'PATH may be an empty directory or the last element of PATH may not exist, in which case ' + 'the client may initialize a repository there.') subparser.add_argument('--append-only', dest='append_only', action='store_true', help='only allow appending to repository segment files') subparser.add_argument('--storage-quota', dest='storage_quota', default=None, diff --git a/src/borg/remote.py b/src/borg/remote.py index 47c59741e..5dc2a495a 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -87,7 +87,7 @@ class ConnectionClosedWithHint(ConnectionClosed): class PathNotAllowed(Error): - """Repository path not allowed""" + """Repository path not allowed: {}""" class InvalidRPCMethod(Error): @@ -178,9 +178,10 @@ class RepositoryServer: # pragma: no cover 'inject_exception', ) - def __init__(self, restrict_to_paths, append_only, storage_quota): + def __init__(self, restrict_to_paths, restrict_to_repositories, append_only, storage_quota): self.repository = None self.restrict_to_paths = restrict_to_paths + self.restrict_to_repositories = restrict_to_repositories # This flag is parsed from the serve command line via Archiver.do_serve, # i.e. it reflects local system policy and generally ranks higher than # whatever the client wants, except when initializing a new repository @@ -348,17 +349,24 @@ class RepositoryServer: # pragma: no cover logging.debug('Resolving repository path %r', path) path = self._resolve_path(path) logging.debug('Resolved repository path to %r', path) + path_with_sep = os.path.join(path, '') # make sure there is a trailing slash (os.sep) if self.restrict_to_paths: # if --restrict-to-path P is given, we make sure that we only operate in/below path P. # for the prefix check, it is important that the compared pathes both have trailing slashes, # so that a path /foobar will NOT be accepted with --restrict-to-path /foo option. - path_with_sep = os.path.join(path, '') # make sure there is a trailing slash (os.sep) for restrict_to_path in self.restrict_to_paths: restrict_to_path_with_sep = os.path.join(os.path.realpath(restrict_to_path), '') # trailing slash if path_with_sep.startswith(restrict_to_path_with_sep): break else: raise PathNotAllowed(path) + if self.restrict_to_repositories: + for restrict_to_repository in self.restrict_to_repositories: + restrict_to_repository_with_sep = os.path.join(os.path.realpath(restrict_to_repository), '') + if restrict_to_repository_with_sep == path_with_sep: + break + else: + raise PathNotAllowed(path) # "borg init" on "borg serve --append-only" (=self.append_only) does not create an append only repo, # while "borg init --append-only" (=append_only) does, regardless of the --append-only (self.append_only) # flag for serve. @@ -383,7 +391,7 @@ class RepositoryServer: # pragma: no cover elif kind == 'IntegrityError': raise IntegrityError(s1) elif kind == 'PathNotAllowed': - raise PathNotAllowed() + raise PathNotAllowed('foo') elif kind == 'ObjectNotFound': raise Repository.ObjectNotFound(s1, s2) elif kind == 'InvalidRPCMethod': @@ -739,7 +747,10 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. else: raise IntegrityError(args[0].decode()) elif error == 'PathNotAllowed': - raise PathNotAllowed() + if old_server: + raise PathNotAllowed('(unknown)') + else: + raise PathNotAllowed(args[0].decode()) elif error == 'ObjectNotFound': if old_server: raise Repository.ObjectNotFound('(not available)', self.location.orig) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 9e207f966..5c0c52a12 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -2861,6 +2861,15 @@ class RemoteArchiverTestCase(ArchiverTestCase): with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo', '--restrict-to-path', path_prefix]): self.cmd('init', '--encryption=repokey', self.repository_location + '_3') + def test_remote_repo_restrict_to_repository(self): + # restricted to repo directory itself: + with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-repository', self.repository_path]): + self.cmd('init', '--encryption=repokey', self.repository_location) + parent_path = os.path.join(self.repository_path, '..') + with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-repository', parent_path]): + with pytest.raises(PathNotAllowed): + self.cmd('init', '--encryption=repokey', self.repository_location) + @unittest.skip('only works locally') def test_debug_put_get_delete_obj(self): pass diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index 4efd0d214..25e112bda 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -815,7 +815,8 @@ class RemoteRepositoryTestCase(RepositoryTestCase): try: self.repository.call('inject_exception', {'kind': 'PathNotAllowed'}) except PathNotAllowed as e: - assert len(e.args) == 0 + assert len(e.args) == 1 + assert e.args[0] == 'foo' try: self.repository.call('inject_exception', {'kind': 'ObjectNotFound'})