mirror of
https://github.com/openSUSE/osc.git
synced 2025-02-03 18:16:17 +01:00
Introduction of new credential management
* new module credentials.py which contains classes and methods to set and get passwords for different backends: - python-keyring - gnomekeyring - ConfigFile based storage The new code should be backward compatible except a minor change in add_section (pass and passx are not removed from the config parser). This affects only callers that do not pass a creds_mgr_descriptor. On initial osc call or initial osc call on new API Url the user now can decide where to store the password (based on the backends available on his system)
This commit is contained in:
parent
c9d85ac248
commit
eb3a3ef0ec
163
osc/conf.py
163
osc/conf.py
@ -64,7 +64,9 @@ except ImportError:
|
||||
|
||||
from . import OscConfigParser
|
||||
from osc import oscerr
|
||||
from osc.util.helper import raw_input
|
||||
from .oscsslexcp import NoSecureSSLError
|
||||
from osc import credentials
|
||||
|
||||
GENERIC_KEYRING = False
|
||||
GNOME_KEYRING = False
|
||||
@ -98,9 +100,9 @@ def _get_processors():
|
||||
return 1
|
||||
|
||||
DEFAULTS = {'apiurl': 'https://api.opensuse.org',
|
||||
'user': 'your_username',
|
||||
'pass': 'your_password',
|
||||
'passx': '',
|
||||
'user': None,
|
||||
'pass': None,
|
||||
'passx': None,
|
||||
'packagecachedir': '/var/tmp/osbuild-packagecache',
|
||||
'su-wrapper': 'sudo',
|
||||
|
||||
@ -322,9 +324,6 @@ apiurl = %(apiurl)s
|
||||
# print call traces in case of errors
|
||||
#traceback = 1
|
||||
|
||||
# use KDE/Gnome/MacOS/Windows keyring for credentials if available
|
||||
#use_keyring = 1
|
||||
|
||||
# check for unversioned/removed files before commit
|
||||
#check_filelist = 1
|
||||
|
||||
@ -353,8 +352,6 @@ apiurl = %(apiurl)s
|
||||
#review_inherit_group = 1
|
||||
|
||||
[%(apiurl)s]
|
||||
user = %(user)s
|
||||
pass = %(pass)s
|
||||
# set aliases for this apiurl
|
||||
# aliases = foo, bar
|
||||
# real name used in .changes, unless the one from osc meta prj <user> will be used
|
||||
@ -366,8 +363,6 @@ pass = %(pass)s
|
||||
# User: mumblegack
|
||||
# Plain text password
|
||||
#pass =
|
||||
# Force using of keyring for this API
|
||||
#keyring = 1
|
||||
"""
|
||||
|
||||
|
||||
@ -728,7 +723,7 @@ def passx_encode(passwd):
|
||||
"""encode plain text password to obfuscated form"""
|
||||
return base64.b64encode(bz2.compress(passwd.encode('ascii'))).decode("ascii")
|
||||
|
||||
def write_initial_config(conffile, entries, custom_template=''):
|
||||
def write_initial_config(conffile, entries, custom_template='', creds_mgr_descriptor=None):
|
||||
"""
|
||||
write osc's intial configuration file. entries is a dict which contains values
|
||||
for the config file (e.g. { 'user' : 'username', 'pass' : 'password' } ).
|
||||
@ -737,33 +732,19 @@ def write_initial_config(conffile, entries, custom_template=''):
|
||||
conf_template = custom_template or new_conf_template
|
||||
config = DEFAULTS.copy()
|
||||
config.update(entries)
|
||||
# at this point use_keyring and gnome_keyring are str objects
|
||||
if config['use_keyring'] == '1' and GENERIC_KEYRING:
|
||||
protocol, host, path = \
|
||||
parse_apisrv_url(None, config['apiurl'])
|
||||
keyring.set_password(host, config['user'], config['pass'])
|
||||
config['pass'] = ''
|
||||
config['passx'] = ''
|
||||
elif config['gnome_keyring'] == '1' and GNOME_KEYRING:
|
||||
protocol, host, path = \
|
||||
parse_apisrv_url(None, config['apiurl'])
|
||||
gnomekeyring.set_network_password_sync(
|
||||
user=config['user'],
|
||||
password=config['pass'],
|
||||
protocol=protocol,
|
||||
server=host,
|
||||
object=path)
|
||||
config['user'] = ''
|
||||
config['pass'] = ''
|
||||
config['passx'] = ''
|
||||
|
||||
sio = StringIO(conf_template.strip() % config)
|
||||
cp = OscConfigParser.OscConfigParser(DEFAULTS)
|
||||
cp.readfp(sio)
|
||||
cp.set(config['apiurl'], 'user', config['user'])
|
||||
if creds_mgr_descriptor:
|
||||
creds_mgr = creds_mgr_descriptor.create(cp)
|
||||
else:
|
||||
creds_mgr = _get_credentials_manager(config['apiurl'], cp)
|
||||
creds_mgr.set_password(config['apiurl'], config['user'], config['pass'])
|
||||
write_config(conffile, cp)
|
||||
|
||||
|
||||
def add_section(filename, url, user, passwd):
|
||||
def add_section(filename, url, user, passwd, creds_mgr_descriptor=None):
|
||||
"""
|
||||
Add a section to config file for new api url.
|
||||
"""
|
||||
@ -774,30 +755,27 @@ def add_section(filename, url, user, passwd):
|
||||
except OscConfigParser.configparser.DuplicateSectionError:
|
||||
# Section might have existed, but was empty
|
||||
pass
|
||||
if config['use_keyring'] and GENERIC_KEYRING:
|
||||
protocol, host, path = parse_apisrv_url(None, url)
|
||||
keyring.set_password(host, user, passwd)
|
||||
cp.set(url, 'keyring', '1')
|
||||
cp.set(url, 'user', user)
|
||||
cp.remove_option(url, 'pass')
|
||||
cp.remove_option(url, 'passx')
|
||||
elif config['gnome_keyring'] and GNOME_KEYRING:
|
||||
protocol, host, path = parse_apisrv_url(None, url)
|
||||
gnomekeyring.set_network_password_sync(
|
||||
user=user,
|
||||
password=passwd,
|
||||
protocol=protocol,
|
||||
server=host,
|
||||
object=path)
|
||||
cp.set(url, 'keyring', '1')
|
||||
cp.remove_option(url, 'pass')
|
||||
cp.remove_option(url, 'passx')
|
||||
cp.set(url, 'user', user)
|
||||
if creds_mgr_descriptor:
|
||||
creds_mgr = creds_mgr_descriptor.create(cp)
|
||||
else:
|
||||
cp.set(url, 'user', user)
|
||||
cp.set(url, 'pass', passwd)
|
||||
creds_mgr = _get_credentials_manager(url, cp)
|
||||
creds_mgr.set_password(url, user, passwd)
|
||||
write_config(filename, cp)
|
||||
|
||||
|
||||
def _get_credentials_manager(url, cp):
|
||||
if cp.has_option(url, credentials.AbstractCredentialsManager.config_entry):
|
||||
return credentials.create_credentials_manager(url, cp)
|
||||
if config['use_keyring'] and GENERIC_KEYRING:
|
||||
return credentials.get_keyring_credentials_manager(cp)
|
||||
elif config['gnome_keyring'] and GNOME_KEYRING:
|
||||
protocol, host, path = parse_apisrv_url(None, url)
|
||||
return credentials.GnomeKeyringCredentialsManager(cp, None)
|
||||
elif cp.get(url, 'passx') is not None:
|
||||
return credentials.ObfuscatedConfigFileCredentialsManager(cp, None)
|
||||
return credentials.PlaintextConfigFileCredentialsManager(cp, None)
|
||||
|
||||
def get_config(override_conffile=None,
|
||||
override_apiurl=None,
|
||||
override_debug=None,
|
||||
@ -874,60 +852,19 @@ def get_config(override_conffile=None,
|
||||
# backward compatiblity
|
||||
scheme, host, path = parse_apisrv_url(config.get('scheme', 'https'), url)
|
||||
apiurl = urljoin(scheme, host, path)
|
||||
user = None
|
||||
password = None
|
||||
if config['use_keyring'] and GENERIC_KEYRING:
|
||||
try:
|
||||
# Read from keyring lib if available
|
||||
user = cp.get(url, 'user', raw=True)
|
||||
password = str(keyring.get_password(host, user))
|
||||
except:
|
||||
# Fallback to file based auth.
|
||||
pass
|
||||
elif config['gnome_keyring'] and GNOME_KEYRING:
|
||||
# Read from gnome keyring if available
|
||||
try:
|
||||
gk_data = gnomekeyring.find_network_password_sync(protocol=scheme, server=host, object=path)
|
||||
if not 'user' in gk_data[0]:
|
||||
raise oscerr.ConfigError('no user found in keyring', conffile)
|
||||
user = gk_data[0]['user']
|
||||
if 'password' in gk_data[0]:
|
||||
password = str(gk_data[0]['password'])
|
||||
else:
|
||||
# this is most likely an error
|
||||
print('warning: no password found in keyring', file=sys.stderr)
|
||||
except gnomekeyring.NoMatchError:
|
||||
# Fallback to file based auth.
|
||||
pass
|
||||
|
||||
if not user is None and len(user) == 0:
|
||||
user = None
|
||||
print('Warning: blank user in the keyring for the ' \
|
||||
'apiurl %s.\nPlease fix your keyring entry.', file=sys.stderr)
|
||||
|
||||
if user is not None and password is None:
|
||||
err = ('no password defined for "%s".\nPlease fix your keyring '
|
||||
'entry or gnome-keyring setup.\nAssuming an empty password.'
|
||||
% url)
|
||||
print(err, file=sys.stderr)
|
||||
password = ''
|
||||
|
||||
# Read credentials from config
|
||||
user = cp.get(url, 'user', raw=True)
|
||||
creds_mgr = _get_credentials_manager(url, cp)
|
||||
# currently, this is only needed for the deprecated gnomekeyring - actually, we
|
||||
# we should use the apiurl instead of url (that's what the old code did), but
|
||||
# this makes things more complex (also, it is very unlikely that url and
|
||||
# apiurl differ)
|
||||
if user is None and hasattr(creds_mgr, 'get_user'):
|
||||
user = creds_mgr.get_user(url)
|
||||
if user is None:
|
||||
#FIXME: this could actually be the ideal spot to take defaults
|
||||
#from the general section.
|
||||
user = cp.get(url, 'user', raw=True) # need to set raw to prevent '%' expansion
|
||||
password = cp.get(url, 'pass', raw=True) # especially on password!
|
||||
try:
|
||||
passwordx = passx_decode(cp.get(url, 'passx', raw=True)) # especially on password!
|
||||
except:
|
||||
passwordx = ''
|
||||
|
||||
if password == None or password == 'your_password':
|
||||
password = ''
|
||||
|
||||
if user is None or user == '':
|
||||
raise oscerr.ConfigError('user is blank for %s, please delete or complete the "user=" entry in %s.' % (apiurl, config['conffile']), config['conffile'])
|
||||
raise oscerr.ConfigError('No user found in section %s' % url, conffile)
|
||||
password = creds_mgr.get_password(url, user)
|
||||
if password is None:
|
||||
raise oscerr.ConfigError('No password found in section %s' % url, conffile)
|
||||
|
||||
if cp.has_option(url, 'http_headers'):
|
||||
http_headers = cp.get(url, 'http_headers')
|
||||
@ -1038,13 +975,25 @@ def identify_conf():
|
||||
def interactive_config_setup(conffile, apiurl, initial=True):
|
||||
user = raw_input('Username: ')
|
||||
passwd = getpass.getpass()
|
||||
if not credentials.has_keyring_support():
|
||||
print('To use keyrings please install python-keyring.')
|
||||
creds_mgr_descriptors = credentials.get_credentials_manager_descriptors()
|
||||
for i, creds_mgr_descr in enumerate(creds_mgr_descriptors, 1):
|
||||
print('%d) %s (%s)' % (i, creds_mgr_descr.name(), creds_mgr_descr.description()))#
|
||||
i = raw_input('Select credentials manager: ')
|
||||
if not i.isdigit():
|
||||
sys.exit('Invalid selection')
|
||||
i = int(i) - 1
|
||||
if i < 0 or i >= len(creds_mgr_descriptors):
|
||||
sys.exit('Invalid selection')
|
||||
creds_mgr_descr = creds_mgr_descriptors[i]
|
||||
if initial:
|
||||
config = {'user': user, 'pass': passwd}
|
||||
if apiurl:
|
||||
config['apiurl'] = apiurl
|
||||
write_initial_config(conffile, config)
|
||||
write_initial_config(conffile, config, creds_mgr_descriptor=creds_mgr_descr)
|
||||
else:
|
||||
add_section(conffile, apiurl, user, passwd)
|
||||
add_section(conffile, apiurl, user, passwd, creds_mgr_descriptor=creds_mgr_descr)
|
||||
|
||||
|
||||
# vim: sw=4 et
|
||||
|
259
osc/credentials.py
Normal file
259
osc/credentials.py
Normal file
@ -0,0 +1,259 @@
|
||||
import importlib
|
||||
import bz2
|
||||
import base64
|
||||
try:
|
||||
from urllib.parse import urlsplit
|
||||
except ImportError:
|
||||
from urlparse import urlsplit
|
||||
try:
|
||||
import keyring
|
||||
except ImportError:
|
||||
keyring = None
|
||||
try:
|
||||
import gnomekeyring
|
||||
except ImportError:
|
||||
gnomekeyring = None
|
||||
|
||||
|
||||
class AbstractCredentialsManagerDescriptor(object):
|
||||
def name(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def description(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def create(self, cp):
|
||||
raise NotImplementedError()
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.name() < other.name()
|
||||
|
||||
|
||||
class AbstractCredentialsManager(object):
|
||||
config_entry = 'credentials_mgr_class'
|
||||
|
||||
def __init__(self, cp, options):
|
||||
super(AbstractCredentialsManager, self).__init__()
|
||||
self._cp = cp
|
||||
self._process_options(options)
|
||||
|
||||
def get_password(self, url, user, defer=True):
|
||||
# If defer is True a callable can be returned
|
||||
# and the password is retrieved if the callable
|
||||
# is called. Implementations are free to ignore
|
||||
# defer parameter and can directly return the password.
|
||||
# If defer is False the password is directly returned.
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_password(self, url, user, password):
|
||||
raise NotImplementedError()
|
||||
|
||||
def delete_password(self, url, user):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _qualified_name(self):
|
||||
return qualified_name(self)
|
||||
|
||||
def _process_options(self, options):
|
||||
pass
|
||||
|
||||
|
||||
class PlaintextConfigFileCredentialsManager(AbstractCredentialsManager):
|
||||
def get_password(self, url, user, defer=True):
|
||||
return self._cp.get(url, 'pass', raw=True)
|
||||
|
||||
def set_password(self, url, user, password):
|
||||
self._cp.set(url, 'pass', password)
|
||||
self._cp.set(url, self.config_entry, self._qualified_name())
|
||||
|
||||
def delete_password(self, url, user):
|
||||
self._cp.remove_option(url, 'pass')
|
||||
|
||||
def _process_options(self, options):
|
||||
if options is not None:
|
||||
raise RuntimeError('options must be None')
|
||||
|
||||
|
||||
class PlaintextConfigFileDescriptor(AbstractCredentialsManagerDescriptor):
|
||||
def name(self):
|
||||
return 'Config file credentials manager'
|
||||
|
||||
def description(self):
|
||||
return 'Store the credentials in the config file (plain text)'
|
||||
|
||||
def create(self, cp):
|
||||
return PlaintextConfigFileCredentialsManager(cp, None)
|
||||
|
||||
|
||||
class ObfuscatedConfigFileCredentialsManager(
|
||||
PlaintextConfigFileCredentialsManager):
|
||||
def get_password(self, url, user, defer=True):
|
||||
passwd = super(self.__class__, self).get_password(url, user)
|
||||
return self.decode_password(passwd)
|
||||
|
||||
def set_password(self, url, user, password):
|
||||
compressed_pw = bz2.compress(password.encode('ascii'))
|
||||
password = base64.b64encode(compressed_pw).decode("ascii")
|
||||
super(self.__class__, self).set_password(url, user, password)
|
||||
|
||||
@classmethod
|
||||
def decode_password(cls, password):
|
||||
compressed_pw = base64.b64decode(password.encode("ascii"))
|
||||
return bz2.decompress(compressed_pw).decode("ascii")
|
||||
|
||||
|
||||
class ObfuscatedConfigFileDescriptor(AbstractCredentialsManagerDescriptor):
|
||||
def name(self):
|
||||
return 'Obfuscated Config file credentials manager'
|
||||
|
||||
def description(self):
|
||||
return 'Store the credentials in the config file (obfuscated)'
|
||||
|
||||
def create(self, cp):
|
||||
return ObfuscatedConfigFileCredentialsManager(cp, None)
|
||||
|
||||
|
||||
class KeyringCredentialsManager(AbstractCredentialsManager):
|
||||
def __init__(self, cp, options, appname='osc'):
|
||||
super(self.__class__, self).__init__(cp, options)
|
||||
self._appname = appname
|
||||
|
||||
def _process_options(self, options):
|
||||
if options is None:
|
||||
raise RuntimeError('options may not be None')
|
||||
self._backend_cls_name = options
|
||||
|
||||
def _load_backend(self):
|
||||
keyring_backend = keyring.core.load_keyring(self._backend_cls_name)
|
||||
keyring.set_keyring(keyring_backend)
|
||||
|
||||
def get_password(self, url, user, defer=True):
|
||||
self._load_backend()
|
||||
return keyring.get_password(self._appname, user)
|
||||
|
||||
def set_password(self, url, user, password):
|
||||
self._load_backend()
|
||||
keyring.set_password(self._appname, user, password)
|
||||
config_value = self._qualified_name() + ':' + self._backend_cls_name
|
||||
self._cp.set(url, self.config_entry, config_value)
|
||||
|
||||
def delete_password(self, url, user):
|
||||
self._load_backend()
|
||||
keyring.delete_password(self._appname, user)
|
||||
|
||||
|
||||
class KeyringCredentialsDescriptor(AbstractCredentialsManagerDescriptor):
|
||||
def __init__(self, keyring_backend):
|
||||
self._keyring_backend = keyring_backend
|
||||
|
||||
def name(self):
|
||||
return self._keyring_backend.name
|
||||
|
||||
def description(self):
|
||||
return 'Backend provided by python-keyring'
|
||||
|
||||
def create(self, cp):
|
||||
qualified_backend_name = qualified_name(self._keyring_backend)
|
||||
return KeyringCredentialsManager(cp, qualified_backend_name)
|
||||
|
||||
|
||||
class GnomeKeyringCredentialsManager(AbstractCredentialsManager):
|
||||
def get_password(self, url, user, defer=True):
|
||||
gk_data = self._keyring_data(url, user)
|
||||
if gk_data is None:
|
||||
return None
|
||||
return gk_data['password']
|
||||
|
||||
def set_password(self, url, user, password):
|
||||
scheme, host, path = self._urlsplit(url)
|
||||
gnomekeyring.set_network_password_sync(
|
||||
user=user,
|
||||
password=password,
|
||||
protocol=scheme,
|
||||
server=host,
|
||||
object=path)
|
||||
self._cp.set(url, self.config_entry, self._qualified_name())
|
||||
|
||||
def delete_password(self, url, user):
|
||||
gk_data = self._keyring_data(url, user)
|
||||
if gk_data is None:
|
||||
return
|
||||
gnomekeyring.item_delete_sync(gk_data['keyring'], gk_data['item_id'])
|
||||
|
||||
def get_user(self, url):
|
||||
gk_data = self._keyring_data(url, None)
|
||||
if gk_data is None:
|
||||
return None
|
||||
return gk_data['user']
|
||||
|
||||
def _keyring_data(self, url, user):
|
||||
scheme, host, path = self._urlsplit(url)
|
||||
try:
|
||||
entries = gnomekeyring.find_network_password_sync(protocol=scheme,
|
||||
server=host,
|
||||
object=path)
|
||||
except gnomekeyring.NoMatchError:
|
||||
return None
|
||||
|
||||
for entry in entries:
|
||||
if 'user' not in entry or 'password' not in entry:
|
||||
continue
|
||||
if user is None or entry['user'] == user:
|
||||
return entry
|
||||
return None
|
||||
|
||||
def _urlsplit(self, url):
|
||||
splitted_url = urlsplit(url)
|
||||
return splitted_url.scheme, splitted_url.netloc, splitted_url.path
|
||||
|
||||
|
||||
class GnomeKeyringCredentialsDescriptor(AbstractCredentialsManagerDescriptor):
|
||||
def name(self):
|
||||
return 'GNOME Keyring Manager (deprecated)'
|
||||
|
||||
def description(self):
|
||||
return 'Deprecated GNOME Keyring Manager. If you use \
|
||||
this we will send you a Dial-In modem'
|
||||
|
||||
def create(self, cp):
|
||||
return GnomeKeyringCredentialsManager(cp, None)
|
||||
|
||||
|
||||
def get_credentials_manager_descriptors():
|
||||
if has_keyring_support():
|
||||
backend_list = keyring.backend.get_all_keyring()
|
||||
else:
|
||||
backend_list = []
|
||||
descriptors = []
|
||||
for backend in backend_list:
|
||||
descriptors.append(KeyringCredentialsDescriptor(backend))
|
||||
descriptors.sort()
|
||||
if gnomekeyring:
|
||||
descriptors.append(GnomeKeyringCredentialsDescriptor())
|
||||
descriptors.append(PlaintextConfigFileDescriptor())
|
||||
descriptors.append(ObfuscatedConfigFileDescriptor())
|
||||
return descriptors
|
||||
|
||||
|
||||
def get_keyring_credentials_manager(cp):
|
||||
keyring_backend = keyring.get_keyring()
|
||||
return KeyringCredentialsManager(cp, qualified_name(keyring_backend))
|
||||
|
||||
|
||||
def create_credentials_manager(url, cp):
|
||||
config_entry = cp.get(url, AbstractCredentialsManager.config_entry)
|
||||
if ':' in config_entry:
|
||||
creds_mgr_cls, options = config_entry.split(':', 1)
|
||||
else:
|
||||
creds_mgr_cls = config_entry
|
||||
options = None
|
||||
mod, cls = creds_mgr_cls.rsplit('.', 1)
|
||||
return getattr(importlib.import_module(mod), cls)(cp, options)
|
||||
|
||||
|
||||
def qualified_name(obj):
|
||||
return obj.__module__ + '.' + obj.__class__.__name__
|
||||
|
||||
|
||||
def has_keyring_support():
|
||||
return keyring is not None
|
Loading…
Reference in New Issue
Block a user