浏览代码

Added option to restrict remote repository access to specific path(s)

With this option remote repository access can be restricted to a
specific path for a specific ssh key using the following line
in ~/.ssh/authorized_keys::

command="attic serve --restrict-to-path /data/clientA" ssh-rsa clientA's key
command="attic serve --restrict-to-path /data/clientB" ssh-rsa clientB's key

Closes #51.
Jonas Borgström 11 年之前
父节点
当前提交
a9fc62cc9a
共有 4 个文件被更改,包括 41 次插入7 次删除
  1. 2 0
      CHANGES
  2. 10 4
      attic/archiver.py
  3. 18 3
      attic/remote.py
  4. 11 0
      attic/testsuite/archiver.py

+ 2 - 0
CHANGES

@@ -8,6 +8,8 @@ Version 0.12
 
 (feature release, released on X)
 
+- Added option to restrict remote repository access to specific path(s):
+  ``attic serve --restrict-to-path X`` (#51)
 - Include "all archives" size information in "--stats" output. (#54)
 - Switch to SI units (Power of 1000 instead 1024) when printing file sizes
 - Added "--stats" option to 'attic delete' and 'attic prune'

+ 10 - 4
attic/archiver.py

@@ -47,8 +47,10 @@ class Archiver:
             else:
                 print(msg, end=' ')
 
-    def do_serve(self):
-        return RepositoryServer().serve()
+    def do_serve(self, args):
+        """Start Attic in server mode. This command is usually not used manually.
+        """
+        return RepositoryServer(restrict_to_paths=args.restrict_to_paths).serve()
 
     def do_init(self, args):
         """Initialize an empty repository
@@ -431,14 +433,18 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
                             help='verbose output')
 
         # We can't use argparse for "serve" since we don't want it to show up in "Available commands"
-        if args and args[0] == 'serve':
-            return self.do_serve()
         if args:
             args = self.preprocess_args(args)
 
         parser = argparse.ArgumentParser(description='Attic %s - Deduplicated Backups' % __version__)
         subparsers = parser.add_subparsers(title='Available commands')
 
+        subparser = subparsers.add_parser('serve', parents=[common_parser],
+                                          description=self.do_serve.__doc__)
+        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 = subparsers.add_parser('init', parents=[common_parser],
                                           description=self.do_init.__doc__)
         subparser.set_defaults(func=self.do_init)

+ 18 - 3
attic/remote.py

@@ -18,10 +18,15 @@ class ConnectionClosed(Error):
     """Connection closed by remote host"""
 
 
+class PathNotAllowed(Error):
+    """Repository path not allowed"""
+
+
 class RepositoryServer(object):
 
-    def __init__(self):
+    def __init__(self, restrict_to_paths):
         self.repository = None
+        self.restrict_to_paths = restrict_to_paths
 
     def serve(self):
         # Make stdin non-blocking
@@ -61,11 +66,19 @@ class RepositoryServer(object):
         path = os.fsdecode(path)
         if path.startswith('/~'):
             path = path[1:]
-        self.repository = Repository(os.path.expanduser(path), create)
+        path = os.path.realpath(os.path.expanduser(path))
+        if self.restrict_to_paths:
+            for restrict_to_path in self.restrict_to_paths:
+                if path.startswith(os.path.realpath(restrict_to_path)):
+                    break
+            else:
+                raise PathNotAllowed(path)
+        self.repository = Repository(path, create)
         return self.repository.id
 
 
 class RemoteRepository(object):
+    extra_test_args = []
 
     class RPCError(Exception):
 
@@ -83,7 +96,7 @@ class RemoteRepository(object):
         self.unpacker = msgpack.Unpacker(use_list=False)
         self.p = None
         if location.host == '__testsuite__':
-            args = [sys.executable, '-m', 'attic.archiver', 'serve']
+            args = [sys.executable, '-m', 'attic.archiver', 'serve'] + self.extra_test_args
         else:
             args = ['ssh']
             if location.port:
@@ -139,6 +152,8 @@ class RemoteRepository(object):
                             raise Repository.CheckNeeded(self.location.orig)
                         elif error == b'IntegrityError':
                             raise IntegrityError(res)
+                        elif error == b'PathNotAllowed':
+                            raise PathNotAllowed(*res)
                         raise self.RPCError(error)
                     else:
                         yield res

+ 11 - 0
attic/testsuite/archiver.py

@@ -13,6 +13,7 @@ from attic.archive import Archive, ChunkBuffer
 from attic.archiver import Archiver
 from attic.crypto import bytes_to_long, num_aes_blocks
 from attic.helpers import Manifest
+from attic.remote import RemoteRepository, PathNotAllowed
 from attic.repository import Repository
 from attic.testsuite import AtticTestCase
 from attic.testsuite.mock import patch
@@ -403,3 +404,13 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase):
 
 class RemoteArchiverTestCase(ArchiverTestCase):
     prefix = '__testsuite__:'
+
+    def test_remote_repo_restrict_to_path(self):
+        self.attic('init', self.repository_location)
+        path_prefix = os.path.dirname(self.repository_path)
+        with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo']):
+            self.assert_raises(PathNotAllowed, lambda: self.attic('init', self.repository_location + '_1'))
+        with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', path_prefix]):
+            self.attic('init', self.repository_location + '_2')
+        with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo', '--restrict-to-path', path_prefix]):
+            self.attic('init', self.repository_location + '_3')