mirror of
https://github.com/samsonjs/vdirsyncer.git
synced 2026-04-27 14:57:41 +00:00
Permissions of status files are now checked
Also vdirsyncer now doesn't leak passwords from the config file into the collection cache. See #213.
This commit is contained in:
parent
2170a4fce2
commit
7ace6fb8f1
4 changed files with 66 additions and 9 deletions
|
|
@ -9,6 +9,11 @@ Package maintainers and users who have to manually update their installation
|
||||||
may want to subscribe to `GitHub's tag feed
|
may want to subscribe to `GitHub's tag feed
|
||||||
<https://github.com/untitaker/vdirsyncer/tags.atom>`_.
|
<https://github.com/untitaker/vdirsyncer/tags.atom>`_.
|
||||||
|
|
||||||
|
Version 0.5.2
|
||||||
|
=============
|
||||||
|
|
||||||
|
- Vdirsyncer now checks and corrects the permissions of status files.
|
||||||
|
|
||||||
Version 0.5.1
|
Version 0.5.1
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,6 @@ General Section
|
||||||
been added on one side or deleted on the other. Relative paths will be
|
been added on one side or deleted on the other. Relative paths will be
|
||||||
interpreted as relative to the configuration file's directory.
|
interpreted as relative to the configuration file's directory.
|
||||||
|
|
||||||
The directory will contain files with very confidential information:
|
|
||||||
Usernames, passwords and listings of collection items may be contained in it.
|
|
||||||
|
|
||||||
- ``password_command`` specifies a command to query for server passwords. The
|
- ``password_command`` specifies a command to query for server passwords. The
|
||||||
command will be called with the username as the first argument, and the
|
command will be called with the username as the first argument, and the
|
||||||
hostname as the second.
|
hostname as the second.
|
||||||
|
|
|
||||||
|
|
@ -53,3 +53,10 @@ def test_discover_command(tmpdir, runner):
|
||||||
assert 'Syncing foobar/b' in lines
|
assert 'Syncing foobar/b' in lines
|
||||||
assert 'Syncing foobar/c' in lines
|
assert 'Syncing foobar/c' in lines
|
||||||
assert 'Syncing foobar/d' in result.output
|
assert 'Syncing foobar/d' in result.output
|
||||||
|
|
||||||
|
# Check for redundant data that is already in the config. This avoids
|
||||||
|
# copying passwords from the config too.
|
||||||
|
assert 'fileext' not in tmpdir \
|
||||||
|
.join('status') \
|
||||||
|
.join('foobar.collections') \
|
||||||
|
.read()
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,10 @@ except ImportError:
|
||||||
import queue
|
import queue
|
||||||
|
|
||||||
|
|
||||||
|
STATUS_PERMISSIONS = 0o600
|
||||||
|
STATUS_DIR_PERMISSIONS = 0o700
|
||||||
|
|
||||||
|
|
||||||
class _StorageIndex(object):
|
class _StorageIndex(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._storages = dict(
|
self._storages = dict(
|
||||||
|
|
@ -173,7 +177,9 @@ def collections_for_pair(status_path, name_a, name_b, pair_name, config_a,
|
||||||
cache_key = _get_collections_cache_key(pair_options, config_a, config_b)
|
cache_key = _get_collections_cache_key(pair_options, config_a, config_b)
|
||||||
if rv and not skip_cache:
|
if rv and not skip_cache:
|
||||||
if rv.get('cache_key', None) == cache_key:
|
if rv.get('cache_key', None) == cache_key:
|
||||||
return rv.get('collections', rv)
|
return list(_expand_collections_cache(
|
||||||
|
rv['collections'], config_a, config_b
|
||||||
|
))
|
||||||
elif rv:
|
elif rv:
|
||||||
cli_logger.info('Detected change in config file, discovering '
|
cli_logger.info('Detected change in config file, discovering '
|
||||||
'collections for {}'.format(pair_name))
|
'collections for {}'.format(pair_name))
|
||||||
|
|
@ -186,11 +192,41 @@ def collections_for_pair(status_path, name_a, name_b, pair_name, config_a,
|
||||||
rv = list(_collections_for_pair_impl(status_path, name_a, name_b,
|
rv = list(_collections_for_pair_impl(status_path, name_a, name_b,
|
||||||
pair_name, config_a, config_b,
|
pair_name, config_a, config_b,
|
||||||
pair_options))
|
pair_options))
|
||||||
|
|
||||||
save_status(status_path, pair_name, data_type='collections',
|
save_status(status_path, pair_name, data_type='collections',
|
||||||
data={'collections': rv, 'cache_key': cache_key})
|
data={
|
||||||
|
'collections': list(
|
||||||
|
_compress_collections_cache(rv, config_a, config_b)
|
||||||
|
),
|
||||||
|
'cache_key': cache_key
|
||||||
|
})
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
def _compress_collections_cache(collections, config_a, config_b):
|
||||||
|
def deduplicate(x, y):
|
||||||
|
rv = {}
|
||||||
|
for key, value in x.items():
|
||||||
|
if key not in y or y[key] != value:
|
||||||
|
rv[key] = value
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
for name, (a, b) in collections:
|
||||||
|
yield name, (deduplicate(a, config_a), deduplicate(b, config_b))
|
||||||
|
|
||||||
|
|
||||||
|
def _expand_collections_cache(collections, config_a, config_b):
|
||||||
|
for name, (a_delta, b_delta) in collections:
|
||||||
|
a = dict(config_a)
|
||||||
|
a.update(a_delta)
|
||||||
|
|
||||||
|
b = dict(config_b)
|
||||||
|
b.update(b_delta)
|
||||||
|
|
||||||
|
yield name, (a, b)
|
||||||
|
|
||||||
|
|
||||||
def _discover_from_config(config):
|
def _discover_from_config(config):
|
||||||
storage_type = config['type']
|
storage_type = config['type']
|
||||||
cls, config = storage_class_from_config(config)
|
cls, config = storage_class_from_config(config)
|
||||||
|
|
@ -384,6 +420,8 @@ def load_status(base_path, pair, collection=None, data_type=None):
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
assert_permissions(path, STATUS_PERMISSIONS)
|
||||||
|
|
||||||
with open(path) as f:
|
with open(path) as f:
|
||||||
try:
|
try:
|
||||||
return dict(json.load(f))
|
return dict(json.load(f))
|
||||||
|
|
@ -404,16 +442,16 @@ def save_status(base_path, pair, collection=None, data_type=None, data=None):
|
||||||
assert data is not None
|
assert data is not None
|
||||||
status_name = get_status_name(pair, collection)
|
status_name = get_status_name(pair, collection)
|
||||||
path = expand_path(os.path.join(base_path, status_name)) + '.' + data_type
|
path = expand_path(os.path.join(base_path, status_name)) + '.' + data_type
|
||||||
base_path = os.path.dirname(path)
|
dirname = os.path.dirname(path)
|
||||||
|
|
||||||
if collection is not None and os.path.isfile(base_path):
|
if collection is not None and os.path.isfile(dirname):
|
||||||
raise CliError('{} is probably a legacy file and could be removed '
|
raise CliError('{} is probably a legacy file and could be removed '
|
||||||
'automatically, but this choice is left to the '
|
'automatically, but this choice is left to the '
|
||||||
'user. If you think this is an error, please file '
|
'user. If you think this is an error, please file '
|
||||||
'a bug at {}'.format(base_path, PROJECT_HOME))
|
'a bug at {}'.format(dirname, PROJECT_HOME))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.makedirs(base_path, 0o750)
|
os.makedirs(dirname, STATUS_DIR_PERMISSIONS)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
if e.errno != errno.EEXIST:
|
if e.errno != errno.EEXIST:
|
||||||
raise
|
raise
|
||||||
|
|
@ -421,6 +459,8 @@ def save_status(base_path, pair, collection=None, data_type=None, data=None):
|
||||||
with atomic_write(path, mode='w', overwrite=True) as f:
|
with atomic_write(path, mode='w', overwrite=True) as f:
|
||||||
json.dump(data, f)
|
json.dump(data, f)
|
||||||
|
|
||||||
|
os.chmod(path, STATUS_PERMISSIONS)
|
||||||
|
|
||||||
|
|
||||||
def storage_class_from_config(config):
|
def storage_class_from_config(config):
|
||||||
config = dict(config)
|
config = dict(config)
|
||||||
|
|
@ -657,3 +697,11 @@ def repair_storage(storage):
|
||||||
seen_uids.add(new_item.uid)
|
seen_uids.add(new_item.uid)
|
||||||
if changed:
|
if changed:
|
||||||
storage.update(href, new_item, etag)
|
storage.update(href, new_item, etag)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_permissions(path, wanted):
|
||||||
|
permissions = os.stat(path).st_mode & 0o777
|
||||||
|
if permissions > wanted:
|
||||||
|
cli_logger.warning('Correcting permissions of {} from {:o} to {:o}'
|
||||||
|
.format(path, permissions, wanted))
|
||||||
|
os.chmod(path, wanted)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue