Skip to content

Commit 7198929

Browse files
committed
cache: Warn user before accessing relocated repositories
This also closes #225
1 parent 78f9ad1 commit 7198929

File tree

5 files changed

+59
-5
lines changed

5 files changed

+59
-5
lines changed

CHANGES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Version 0.15
77
------------
88

99
(feature release, released on X)
10+
- Require approval before accessing relocated/moved repository (#271)
1011
- Require approval before accessing previously unknown unencrypted repositories (#271)
1112
- Fix issue with hash index files larger than 2GB.
1213
- Fix Python 3.2 compatibility issue with noatime open() (#164)

attic/cache.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class RepositoryReplay(Error):
2121
class CacheInitAbortedError(Error):
2222
"""Cache initialization aborted"""
2323

24+
class RepositoryAccessAborted(Error):
25+
"""Repository access aborted"""
2426

2527
class EncryptionMethodMismatch(Error):
2628
"""Repository encryption method changed since last acccess, refusing to continue
@@ -34,15 +36,20 @@ def __init__(self, repository, key, manifest, path=None, sync=True, warn_if_unen
3436
self.key = key
3537
self.manifest = manifest
3638
self.path = path or os.path.join(get_cache_dir(), hexlify(repository.id).decode('ascii'))
39+
# Warn user before sending data to a never seen before unencrypted repository
3740
if not os.path.exists(self.path):
3841
if warn_if_unencrypted and isinstance(key, PlaintextKey):
39-
if 'ATTIC_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK' not in os.environ:
40-
print("""Warning: Attempting to access a previously unknown unencrypted repository\n""", file=sys.stderr)
41-
answer = input('Do you want to continue? [yN] ')
42-
if not (answer and answer in 'Yy'):
43-
raise self.CacheInitAbortedError()
42+
if not self._confirm('Warning: Attempting to access a previously unknown unencrypted repository',
43+
'ATTIC_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK'):
44+
raise self.CacheInitAbortedError()
4445
self.create()
4546
self.open()
47+
# Warn user before sending data to a relocated repository
48+
if self.previous_location and self.previous_location != repository._location.canonical_path():
49+
msg = 'Warning: The repository at location {} was previously located at {}'.format(repository._location.canonical_path(), self.previous_location)
50+
if not self._confirm(msg, 'ATTIC_RELOCATED_REPO_ACCESS_IS_OK'):
51+
raise self.RepositoryAccessAborted()
52+
4653
if sync and self.manifest.id != self.manifest_id:
4754
# If repository is older than the cache something fishy is going on
4855
if self.timestamp and self.timestamp > manifest.timestamp:
@@ -56,6 +63,16 @@ def __init__(self, repository, key, manifest, path=None, sync=True, warn_if_unen
5663
def __del__(self):
5764
self.close()
5865

66+
def _confirm(self, message, env_var_override=None):
67+
print(message, file=sys.stderr)
68+
if env_var_override and os.environ.get(env_var_override):
69+
print("Yes (From {})".format(env_var_override))
70+
return True
71+
if sys.stdin.isatty():
72+
return False
73+
answer = input('Do you want to continue? [yN] ')
74+
return answer and answer in 'Yy'
75+
5976
def create(self):
6077
"""Create a new empty cache at `path`
6178
"""
@@ -86,12 +103,14 @@ def open(self):
86103
self.manifest_id = unhexlify(self.config.get('cache', 'manifest'))
87104
self.timestamp = self.config.get('cache', 'timestamp', fallback=None)
88105
self.key_type = self.config.get('cache', 'key_type', fallback=None)
106+
self.previous_location = self.config.get('cache', 'previous_location', fallback=None)
89107
self.chunks = ChunkIndex.read(os.path.join(self.path, 'chunks').encode('utf-8'))
90108
self.files = None
91109

92110
def close(self):
93111
if self.lock:
94112
self.lock.release()
113+
self.lock = None
95114

96115
def _read_files(self):
97116
self.files = {}
@@ -134,6 +153,7 @@ def commit(self):
134153
self.config.set('cache', 'manifest', hexlify(self.manifest.id).decode('ascii'))
135154
self.config.set('cache', 'timestamp', self.manifest.timestamp)
136155
self.config.set('cache', 'key_type', str(self.key.TYPE))
156+
self.config.set('cache', 'previous_location', self.repository._location.canonical_path())
137157
with open(os.path.join(self.path, 'config'), 'w') as fd:
138158
self.config.write(fd)
139159
self.chunks.write(os.path.join(self.path, 'chunks').encode('utf-8'))

attic/helpers.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,21 @@ def to_key_filename(self):
457457
def __repr__(self):
458458
return "Location(%s)" % self
459459

460+
def canonical_path(self):
461+
if self.proto == 'file':
462+
return self.path
463+
else:
464+
if self.path and self.path.startswith('~'):
465+
path = '/' + self.path
466+
elif self.path and not self.path.startswith('/'):
467+
path = '/~/' + self.path
468+
else:
469+
path = self.path
470+
return 'ssh://{}{}{}{}'.format('{}@'.format(self.user) if self.user else '',
471+
self.host,
472+
':{}'.format(self.port) if self.port else '',
473+
path)
474+
460475

461476
def location_validator(archive=None):
462477
def validator(text):

attic/testsuite/archiver.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,16 @@ def test_repository_swap_detection(self):
209209
self.assert_equal(repository_id, self._extract_repository_id(self.repository_path))
210210
self.assert_raises(Cache.EncryptionMethodMismatch, lambda :self.attic('create', self.repository_location + '::test.2', 'input'))
211211

212+
def test_repository_swap_detection2(self):
213+
self.create_test_files()
214+
self.attic('init', '--encryption=none', self.repository_location + '_unencrypted')
215+
os.environ['ATTIC_PASSPHRASE'] = 'passphrase'
216+
self.attic('init', '--encryption=passphrase', self.repository_location + '_encrypted')
217+
self.attic('create', self.repository_location + '_encrypted::test', 'input')
218+
shutil.rmtree(self.repository_path + '_encrypted')
219+
os.rename(self.repository_path + '_unencrypted', self.repository_path + '_encrypted')
220+
self.assert_raises(Cache.RepositoryAccessAborted, lambda :self.attic('create', self.repository_location + '_encrypted::test.2', 'input'))
221+
212222
def test_strip_components(self):
213223
self.attic('init', self.repository_location)
214224
self.create_regular_file('dir/file')

attic/testsuite/helpers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ def test(self):
5151
)
5252
self.assert_raises(ValueError, lambda: Location('ssh://localhost:22/path:archive'))
5353

54+
def test_canonical_path(self):
55+
locations = ['some/path::archive', 'file://some/path::archive', 'host:some/path::archive',
56+
'host:~user/some/path::archive', 'ssh://host/some/path::archive',
57+
'ssh://user@host:1234/some/path::archive']
58+
for location in locations:
59+
self.assert_equal(Location(location).canonical_path(),
60+
Location(Location(location).canonical_path()).canonical_path())
61+
5462

5563
class FormatTimedeltaTestCase(AtticTestCase):
5664

0 commit comments

Comments
 (0)