From 474600cf2a3aee95d691878ea35de571c081891d Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 13 May 2013 23:14:49 +0200 Subject: [PATCH 1/4] config: Add keyring module with fetch and set. New config submodule for fetching secrets from a secrets service over dbus. To facilitate this we add a section and key to the stored attributes, allowing us to fetch all mopidy values from the keyring and map them to the correct config values. A helper for setting values is also added. Due to differences in in the secrets dbus API across versions of the APIs and various states of support in the secrets service we try and open the default, login and finally session keyrings for storage. Locked keyrings will be dismissed for all operations. --- mopidy/config/keyring.py | 166 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 mopidy/config/keyring.py diff --git a/mopidy/config/keyring.py b/mopidy/config/keyring.py new file mode 100644 index 00000000..29fccb43 --- /dev/null +++ b/mopidy/config/keyring.py @@ -0,0 +1,166 @@ +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 +# version fixed by 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('Keyring lookup failed as D-Bus not installed.') + return None + + bus = _bus() + if not _secrets_running(bus): + logger.debug('Keyring lookup failed as Secrets service not running.') + return None + + service = _serivce(bus) + session = service.OpenSession('plain', EMPTY_STRING)[1] + items, locked = service.SearchItems({'service': 'mopidy'}) + + if not locked and not items: + return None + + 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('Keyring lookup failed as it is locked.') + return None + + config = {} + 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) + config.setdefault(attrs['section'], {})[attrs['key']] = bytes(value) + return config + + +def set(section, key, value): + """Store a secret config value for a given section/key. + + Indicates if storage failed or succeded. + """ + if not dbus: + logger.debug('Saving secret %s/%s failed as D-Bus not installed.', + section, key) + return False + + bus = _bus() + if not _secrets_running(bus): + logger.debug( + 'Saving secret %s/%s failed as Secrets service not running.', + section, key) + return False + + service = _serivce(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 secret %s/%s failed: %s', section, key, e) + return False + + if prompt == '/': + return True + + _prompt(bus, prompt).Dismiss() + logger.debug('Saving secret %s/%s failed as keyring is locked', + section, key) + return False + + +def _bus(): + if not dbus: + return None + try: + return dbus.SessionBus() + except dbus.exceptions.DBusException as e: + logger.debug('Unable to connect to dbus: %s', e) + return None + + +def _secrets_running(bus): + return bus and bus.name_has_owner('org.freedesktop.secrets') + + +def _serivce(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) From b88cf4a8b21c5d6fe7e49a6803b0c353b8d455f2 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 13 May 2013 23:51:17 +0200 Subject: [PATCH 2/4] config: Integrate keyring values with overrides. --- mopidy/config/__init__.py | 5 +++-- mopidy/config/keyring.py | 14 +++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) 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 index 29fccb43..757b9a24 100644 --- a/mopidy/config/keyring.py +++ b/mopidy/config/keyring.py @@ -22,19 +22,19 @@ else: def fetch(): if not dbus: logger.debug('Keyring lookup failed as D-Bus not installed.') - return None + return [] bus = _bus() if not _secrets_running(bus): logger.debug('Keyring lookup failed as Secrets service not running.') - return None + return [] service = _serivce(bus) session = service.OpenSession('plain', EMPTY_STRING)[1] items, locked = service.SearchItems({'service': 'mopidy'}) if not locked and not items: - return None + return [] if locked: # There is a chance we can unlock without prompting the users... @@ -42,15 +42,15 @@ def fetch(): if prompt != '/': _prompt(bus, prompt).Dismiss() logger.debug('Keyring lookup failed as it is locked.') - return None + return [] - config = {} + 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) - config.setdefault(attrs['section'], {})[attrs['key']] = bytes(value) - return config + result.append((attrs['section'], attrs['key'], bytes(value))) + return result def set(section, key, value): From 9bd4d90e418dcda70a4d1dfc547072c065b7cd94 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 14 May 2013 00:21:05 +0200 Subject: [PATCH 3/4] config: Keyring debug logging cleanup --- mopidy/config/keyring.py | 47 +++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/mopidy/config/keyring.py b/mopidy/config/keyring.py index 757b9a24..8cf78d37 100644 --- a/mopidy/config/keyring.py +++ b/mopidy/config/keyring.py @@ -21,12 +21,18 @@ else: def fetch(): if not dbus: - logger.debug('Keyring lookup failed as D-Bus not installed.') + logger.debug('Fetching from keyring failed: dbus not installed.') return [] - bus = _bus() - if not _secrets_running(bus): - logger.debug('Keyring lookup failed as Secrets service not running.') + 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 = _serivce(bus) @@ -41,7 +47,7 @@ def fetch(): items, prompt = service.Unlock(locked) if prompt != '/': _prompt(bus, prompt).Dismiss() - logger.debug('Keyring lookup failed as it is locked.') + logger.debug('Fetching from keyring failed: keyring is locked.') return [] result = [] @@ -59,14 +65,19 @@ def set(section, key, value): Indicates if storage failed or succeded. """ if not dbus: - logger.debug('Saving secret %s/%s failed as D-Bus not installed.', + logger.debug('Saving %s/%s to keyring failed: dbus not installed.', section, key) return False - bus = _bus() - if not _secrets_running(bus): + 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 secret %s/%s failed as Secrets service not running.', + 'Saving %s/%s to keyring failed: secrets service not running.', section, key) return False @@ -90,32 +101,18 @@ def set(section, key, value): item, prompt = collection.CreateItem(properties, secret, True) except dbus.exceptions.DBusException as e: # TODO: catch IsLocked errors etc. - logger.debug('Saving secret %s/%s failed: %s', section, key, e) + 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 as keyring is locked', + logger.debug('Saving secret %s/%s failed: Keyring is locked', section, key) return False -def _bus(): - if not dbus: - return None - try: - return dbus.SessionBus() - except dbus.exceptions.DBusException as e: - logger.debug('Unable to connect to dbus: %s', e) - return None - - -def _secrets_running(bus): - return bus and bus.name_has_owner('org.freedesktop.secrets') - - def _serivce(bus): return _interface(bus, '/org/freedesktop/secrets', 'org.freedesktop.Secret.Service') From cdbe1c9cd19f6542a1ef6079ee641a47f7892328 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 14 May 2013 21:24:37 +0200 Subject: [PATCH 4/4] config: Review fixes. --- mopidy/config/keyring.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/mopidy/config/keyring.py b/mopidy/config/keyring.py index 8cf78d37..169ffdd1 100644 --- a/mopidy/config/keyring.py +++ b/mopidy/config/keyring.py @@ -11,7 +11,7 @@ except ImportError: # XXX: Hack to workaround introspection bug caused by gnome-keyring, should be -# version fixed by 3.5 per: +# 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) @@ -35,7 +35,7 @@ def fetch(): 'Fetching from keyring failed: secrets service not running.') return [] - service = _serivce(bus) + service = _service(bus) session = service.OpenSession('plain', EMPTY_STRING)[1] items, locked = service.SearchItems({'service': 'mopidy'}) @@ -62,7 +62,7 @@ def fetch(): def set(section, key, value): """Store a secret config value for a given section/key. - Indicates if storage failed or succeded. + Indicates if storage failed or succeeded. """ if not dbus: logger.debug('Saving %s/%s to keyring failed: dbus not installed.', @@ -81,7 +81,7 @@ def set(section, key, value): section, key) return False - service = _serivce(bus) + service = _service(bus) collection = _collection(bus) if not collection: return False @@ -113,16 +113,16 @@ def set(section, key, value): return False -def _serivce(bus): +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. +# 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