Merge pull request #449 from adamcik/feature/keyring
Add keyring support via dbus secret service
This commit is contained in:
commit
bdbe2b32ae
@ -5,6 +5,7 @@ import io
|
|||||||
import logging
|
import logging
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
|
from mopidy.config import keyring
|
||||||
from mopidy.config.schemas import * # noqa
|
from mopidy.config.schemas import * # noqa
|
||||||
from mopidy.config.types import * # noqa
|
from mopidy.config.types import * # noqa
|
||||||
from mopidy.utils import path
|
from mopidy.utils import path
|
||||||
@ -47,7 +48,7 @@ def load(files, extensions, overrides):
|
|||||||
config_dir = os.path.dirname(__file__)
|
config_dir = os.path.dirname(__file__)
|
||||||
defaults = [read(os.path.join(config_dir, 'default.conf'))]
|
defaults = [read(os.path.join(config_dir, 'default.conf'))]
|
||||||
defaults.extend(e.get_default_config() for e in extensions)
|
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 = _schemas[:]
|
||||||
schemas.extend(e.get_config_schema() for e in extensions)
|
schemas.extend(e.get_config_schema() for e in extensions)
|
||||||
@ -101,7 +102,7 @@ def _load(files, defaults, overrides):
|
|||||||
for section in parser.sections():
|
for section in parser.sections():
|
||||||
raw_config[section] = dict(parser.items(section))
|
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
|
raw_config.setdefault(section, {})[key] = value
|
||||||
|
|
||||||
return raw_config
|
return raw_config
|
||||||
|
|||||||
163
mopidy/config/keyring.py
Normal file
163
mopidy/config/keyring.py
Normal file
@ -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)
|
||||||
Loading…
Reference in New Issue
Block a user