Pārlūkot izejas kodu

Merge pull request #2590 from enkore/issue/2589

serve: add --restrict-to-repository
enkore 8 gadi atpakaļ
vecāks
revīzija
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
 The quota is enforcible only if *all* :ref:`borg_serve` versions
 accessible to clients support quotas (see next section). Further, quota is
 accessible to clients support quotas (see next section). Further, quota is
 per repository. Therefore, ensure clients can only access a defined set of repositories
 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
 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()``
 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."""
         """Start in server mode. This command is usually not used manually."""
         return RepositoryServer(
         return RepositoryServer(
             restrict_to_paths=args.restrict_to_paths,
             restrict_to_paths=args.restrict_to_paths,
+            restrict_to_repositories=args.restrict_to_repositories,
             append_only=args.append_only,
             append_only=args.append_only,
             storage_quota=args.storage_quota,
             storage_quota=args.storage_quota,
         ).serve()
         ).serve()
@@ -2339,6 +2340,14 @@ class Archiver:
                                metavar='PATH', help='restrict repository access to PATH. '
                                metavar='PATH', help='restrict repository access to PATH. '
                                                     'Can be specified multiple times to allow the client access to several directories. '
                                                     '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.')
                                                     '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',
         subparser.add_argument('--append-only', dest='append_only', action='store_true',
                                help='only allow appending to repository segment files')
                                help='only allow appending to repository segment files')
         subparser.add_argument('--storage-quota', dest='storage_quota', default=None,
         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):
 class PathNotAllowed(Error):
-    """Repository path not allowed"""
+    """Repository path not allowed: {}"""
 
 
 
 
 class InvalidRPCMethod(Error):
 class InvalidRPCMethod(Error):
@@ -178,9 +178,10 @@ class RepositoryServer:  # pragma: no cover
         'inject_exception',
         '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.repository = None
         self.restrict_to_paths = restrict_to_paths
         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,
         # 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
         # i.e. it reflects local system policy and generally ranks higher than
         # whatever the client wants, except when initializing a new repository
         # 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)
         logging.debug('Resolving repository path %r', path)
         path = self._resolve_path(path)
         path = self._resolve_path(path)
         logging.debug('Resolved repository path to %r', 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 self.restrict_to_paths:
             # if --restrict-to-path P is given, we make sure that we only operate in/below path P.
             # 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,
             # 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.
             # 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:
             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
                 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):
                 if path_with_sep.startswith(restrict_to_path_with_sep):
                     break
                     break
             else:
             else:
                 raise PathNotAllowed(path)
                 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,
         # "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)
         # while "borg init --append-only" (=append_only) does, regardless of the --append-only (self.append_only)
         # flag for serve.
         # flag for serve.
@@ -383,7 +391,7 @@ class RepositoryServer:  # pragma: no cover
         elif kind == 'IntegrityError':
         elif kind == 'IntegrityError':
             raise IntegrityError(s1)
             raise IntegrityError(s1)
         elif kind == 'PathNotAllowed':
         elif kind == 'PathNotAllowed':
-            raise PathNotAllowed()
+            raise PathNotAllowed('foo')
         elif kind == 'ObjectNotFound':
         elif kind == 'ObjectNotFound':
             raise Repository.ObjectNotFound(s1, s2)
             raise Repository.ObjectNotFound(s1, s2)
         elif kind == 'InvalidRPCMethod':
         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:
                 else:
                     raise IntegrityError(args[0].decode())
                     raise IntegrityError(args[0].decode())
             elif error == 'PathNotAllowed':
             elif error == 'PathNotAllowed':
-                raise PathNotAllowed()
+                if old_server:
+                    raise PathNotAllowed('(unknown)')
+                else:
+                    raise PathNotAllowed(args[0].decode())
             elif error == 'ObjectNotFound':
             elif error == 'ObjectNotFound':
                 if old_server:
                 if old_server:
                     raise Repository.ObjectNotFound('(not available)', self.location.orig)
                     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]):
         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')
             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')
     @unittest.skip('only works locally')
     def test_debug_put_get_delete_obj(self):
     def test_debug_put_get_delete_obj(self):
         pass
         pass

+ 2 - 1
src/borg/testsuite/repository.py

@@ -815,7 +815,8 @@ class RemoteRepositoryTestCase(RepositoryTestCase):
         try:
         try:
             self.repository.call('inject_exception', {'kind': 'PathNotAllowed'})
             self.repository.call('inject_exception', {'kind': 'PathNotAllowed'})
         except PathNotAllowed as e:
         except PathNotAllowed as e:
-            assert len(e.args) == 0
+            assert len(e.args) == 1
+            assert e.args[0] == 'foo'
 
 
         try:
         try:
             self.repository.call('inject_exception', {'kind': 'ObjectNotFound'})
             self.repository.call('inject_exception', {'kind': 'ObjectNotFound'})