diff --git a/mopidy/config/__init__.py b/mopidy/config/__init__.py index 0d9b9e7a..e9ae7d86 100644 --- a/mopidy/config/__init__.py +++ b/mopidy/config/__init__.py @@ -5,6 +5,7 @@ import io import logging import os.path +from mopidy.config import keyring from mopidy.config.schemas import * # noqa from mopidy.config.types import * # noqa from mopidy.utils import path @@ -47,7 +48,7 @@ def load(files, extensions, overrides): config_dir = os.path.dirname(__file__) defaults = [read(os.path.join(config_dir, 'default.conf'))] defaults.extend(e.get_default_config() for e in extensions) - raw_config = _load(files, defaults, overrides) + raw_config = _load(files, defaults, keyring.fetch() + (overrides or [])) schemas = _schemas[:] schemas.extend(e.get_config_schema() for e in extensions) @@ -101,7 +102,7 @@ def _load(files, defaults, overrides): for section in parser.sections(): raw_config[section] = dict(parser.items(section)) - for section, key, value in overrides or []: + for section, key, value in overrides: raw_config.setdefault(section, {})[key] = value return raw_config diff --git a/mopidy/config/keyring.py b/mopidy/config/keyring.py new file mode 100644 index 00000000..169ffdd1 --- /dev/null +++ b/mopidy/config/keyring.py @@ -0,0 +1,163 @@ +from __future__ import unicode_literals + +import logging + +logger = logging.getLogger('mopidy.config.keyring') + +try: + import dbus +except ImportError: + dbus = None + + +# XXX: Hack to workaround introspection bug caused by gnome-keyring, should be +# fixed by version 3.5 per: +# https://git.gnome.org/browse/gnome-keyring/commit/?id=5dccbe88eb94eea9934e2b7 +if dbus: + EMPTY_STRING = dbus.String('', variant_level=1) +else: + EMPTY_STRING = '' + + +def fetch(): + if not dbus: + logger.debug('Fetching from keyring failed: dbus not installed.') + return [] + + try: + bus = dbus.SessionBus() + except dbus.exceptions.DBusException as e: + logger.debug('Fetching from keyring failed: %s', e) + return [] + + if not bus.name_has_owner('org.freedesktop.secrets'): + logger.debug( + 'Fetching from keyring failed: secrets service not running.') + return [] + + service = _service(bus) + session = service.OpenSession('plain', EMPTY_STRING)[1] + items, locked = service.SearchItems({'service': 'mopidy'}) + + if not locked and not items: + return [] + + if locked: + # There is a chance we can unlock without prompting the users... + items, prompt = service.Unlock(locked) + if prompt != '/': + _prompt(bus, prompt).Dismiss() + logger.debug('Fetching from keyring failed: keyring is locked.') + return [] + + result = [] + secrets = service.GetSecrets(items, session, byte_arrays=True) + for item_path, values in secrets.iteritems(): + session_path, parameters, value, content_type = values + attrs = _item_attributes(bus, item_path) + result.append((attrs['section'], attrs['key'], bytes(value))) + return result + + +def set(section, key, value): + """Store a secret config value for a given section/key. + + Indicates if storage failed or succeeded. + """ + if not dbus: + logger.debug('Saving %s/%s to keyring failed: dbus not installed.', + section, key) + return False + + try: + bus = dbus.SessionBus() + except dbus.exceptions.DBusException as e: + logger.debug('Saving %s/%s to keyring failed: %s', section, key, e) + return False + + if not bus.name_has_owner('org.freedesktop.secrets'): + logger.debug( + 'Saving %s/%s to keyring failed: secrets service not running.', + section, key) + return False + + service = _service(bus) + collection = _collection(bus) + if not collection: + return False + + if isinstance(value, unicode): + value = value.encode('utf-8') + + session = service.OpenSession('plain', EMPTY_STRING)[1] + secret = dbus.Struct((session, '', dbus.ByteArray(value), + 'plain/text; charset=utf8')) + label = 'mopidy: %s/%s' % (section, key) + attributes = {'service': 'mopidy', 'section': section, 'key': key} + properties = {'org.freedesktop.Secret.Item.Label': label, + 'org.freedesktop.Secret.Item.Attributes': attributes} + + try: + item, prompt = collection.CreateItem(properties, secret, True) + except dbus.exceptions.DBusException as e: + # TODO: catch IsLocked errors etc. + logger.debug('Saving %s/%s to keyring failed: %s', section, key, e) + return False + + if prompt == '/': + return True + + _prompt(bus, prompt).Dismiss() + logger.debug('Saving secret %s/%s failed: Keyring is locked', + section, key) + return False + + +def _service(bus): + return _interface(bus, '/org/freedesktop/secrets', + 'org.freedesktop.Secret.Service') + + +# NOTE: depending on versions and setup 'default' might not exists, so try and +# use it but fall back to the 'login' collection, and finally the 'session' one +# if all else fails. We should probably create a keyring/collection setting +# that allows users to set this so they have control over where their secrets +# get stored. +def _collection(bus): + for name in 'aliases/default', 'collection/login', 'collection/session': + path = '/org/freedesktop/secrets/' + name + if _collection_exists(bus, path): + break + else: + return None + return _interface(bus, path, 'org.freedesktop.Secret.Collection') + + +# NOTE: Hack to probe if a given collection actually exists. Needed to work +# around an introspection bug in setting passwords for non-existant aliases. +def _collection_exists(bus, path): + try: + item = _interface(bus, path, 'org.freedesktop.DBus.Properties') + item.Get('org.freedesktop.Secret.Collection', 'Label') + return True + except dbus.exceptions.DBusException: + return False + + +# NOTE: We could call prompt.Prompt('') to unlock the keyring when it is not +# '/', but we would then also have to arrange to setup signals to wait until +# this has been completed. So for now we just dismiss the prompt and expect +# keyrings to be unlocked. +def _prompt(bus, path): + return _interface(bus, path, 'Prompt') + + +def _item_attributes(bus, path): + item = _interface(bus, path, 'org.freedesktop.DBus.Properties') + result = item.Get('org.freedesktop.Secret.Item', 'Attributes') + return dict((bytes(k), bytes(v)) for k, v in result.iteritems()) + + +def _interface(bus, path, interface): + obj = bus.get_object('org.freedesktop.secrets', path) + return dbus.Interface(obj, interface)