Explorar el Código

Merge pull request #2590 from enkore/issue/2589

serve: add --restrict-to-repository
enkore hace 8 años
padre
commit
372eb40089

+ 1 - 1
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()``

+ 9 - 0
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,

+ 16 - 5
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)

+ 9 - 0
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

+ 2 - 1
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'})