1
0
Fork 0
mirror of https://github.com/borgbackup/borg.git synced 2025-03-19 02:15:49 +00:00

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.
This commit is contained in:
Jonas Borgström 2014-03-24 21:28:59 +01:00
parent 3d53e00116
commit a9fc62cc9a
4 changed files with 41 additions and 7 deletions

View file

@ -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'

View file

@ -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)

View file

@ -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

View file

@ -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')