mirror of
https://github.com/borgbackup/borg.git
synced 2025-03-20 02:45:42 +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:
parent
3d53e00116
commit
a9fc62cc9a
4 changed files with 41 additions and 7 deletions
2
CHANGES
2
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'
|
||||
|
|
|
@ -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,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
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Add table
Reference in a new issue