1
0
mirror of https://github.com/openSUSE/osc.git synced 2024-11-10 14:56:14 +01:00

Merge pull request #625 from openSUSE/pw_redesign

Password handling redesign
This commit is contained in:
Marco Strigl 2019-08-29 16:17:43 +02:00 committed by GitHub
commit e8d9fb52f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 477 additions and 229 deletions

View File

@ -146,27 +146,22 @@ class Osc(cmdln.Cmdln):
except oscerr.NoConfigfile as e:
print(e.msg, file=sys.stderr)
print('Creating osc configuration file %s ...' % e.file, file=sys.stderr)
import getpass
config = {}
config['user'] = raw_input('Username: ')
config['pass'] = getpass.getpass()
if self.options.no_keyring:
config['use_keyring'] = '0'
if self.options.no_gnome_keyring:
config['gnome_keyring'] = '0'
apiurl = conf.DEFAULTS['apiurl']
if self.options.apiurl:
config['apiurl'] = self.options.apiurl
conf.write_initial_config(e.file, config)
apiurl = self.options.apiurl
conf.interactive_config_setup(e.file, apiurl)
print('done', file=sys.stderr)
if try_again:
self.postoptparse(try_again = False)
except oscerr.ConfigMissingApiurl as e:
print(e.msg, file=sys.stderr)
import getpass
user = raw_input('Username: ')
passwd = getpass.getpass()
conf.add_section(e.file, e.url, user, passwd)
conf.interactive_config_setup(e.file, e.url, initial=False)
if try_again:
self.postoptparse(try_again = False)
except oscerr.ConfigMissingCredentialsError as e:
print(e.msg)
print('Please enter new credentials.')
conf.interactive_config_setup(e.file, e.url, initial=False)
if try_again:
self.postoptparse(try_again = False)
@ -8975,6 +8970,10 @@ Please submit there instead, or use --nodevelproject to force direct submission.
help='indicates that the config value should be read from stdin')
@cmdln.option('-p', '--prompt', action='store_true',
help='prompt for a value')
@cmdln.option('--change-password', action='store_true',
help='Change password')
@cmdln.option('--select-password-store', action='store_true',
help='Change the password store')
@cmdln.option('--no-echo', action='store_true',
help='prompt for a value but do not echo entered characters')
@cmdln.option('--dump', action='store_true',
@ -8994,6 +8993,15 @@ Please submit there instead, or use --nodevelproject to force direct submission.
${cmd_usage}
${cmd_option_list}
"""
prompt_value = 'Value: '
if opts.change_password:
opts.no_echo = True
opts.prompt = True
opts.select_password_store = True
prompt_value = 'Password: '
if len(args) != 1:
raise oscerr.WrongArgs('--change-password only needs the apiurl')
args = [args[0], 'pass']
if len(args) < 2 and not (opts.dump or opts.dump_full):
raise oscerr.WrongArgs('Too few arguments')
elif opts.dump or opts.dump_full:
@ -9027,24 +9035,27 @@ Please submit there instead, or use --nodevelproject to force direct submission.
elif opts.no_echo or opts.prompt:
if opts.no_echo:
import getpass
inp = getpass.getpass('Value: ').strip()
inp = getpass.getpass(prompt_value).strip()
else:
inp = raw_input('Value: ').strip()
inp = raw_input(prompt_value).strip()
if not inp:
raise oscerr.WrongArgs('error: no value was entered')
val = [inp]
opt, newval = conf.config_set_option(section, opt, ' '.join(val), delete=opts.delete, update=True)
creds_mgr_descr = None
if opt == 'pass' and opts.select_password_store:
creds_mgr_descr = conf.select_credentials_manager_descr()
orig_opt = opt
opt, newval = conf.config_set_option(section, opt, ' '.join(val), delete=opts.delete, update=True, creds_mgr_descr=creds_mgr_descr)
if newval is None and opts.delete:
print('\'%s\': \'%s\' got removed' % (section, opt))
elif newval is None:
print('\'%s\': \'%s\' is not set' % (section, opt))
else:
if opts.no_echo:
if orig_opt == 'pass':
print('Password has been changed.')
elif opts.no_echo:
# supress value
print('\'%s\': set \'%s\'' % (section, opt))
elif opt == 'pass' and not conf.config['plaintext_passwd'] and newval == 'your_password':
opt, newval = conf.config_set_option(section, 'passx')
print('\'%s\': \'pass\' was rewritten to \'passx\': \'%s\'' % (section, newval))
else:
print('\'%s\': \'%s\' is set to \'%s\'' % (section, opt, newval))

View File

@ -43,6 +43,7 @@ import re
import sys
import ssl
import warnings
import getpass
try:
from http.cookiejar import LWPCookieJar, CookieJar
@ -63,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
@ -76,12 +79,7 @@ except:
import gobject
gobject.set_application_name('osc')
import gnomekeyring
if os.environ['GNOME_DESKTOP_SESSION_ID']:
# otherwise gnome keyring bindings spit out errors, when you have
# it installed, but you are not under gnome
# (even though hundreds of gnome-keyring daemons got started in parallel)
# another option would be to support kwallet here
GNOME_KEYRING = gnomekeyring.is_available()
GNOME_KEYRING = gnomekeyring.is_available()
except:
pass
@ -97,9 +95,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',
@ -151,8 +149,8 @@ DEFAULTS = {'apiurl': 'https://api.opensuse.org',
'checkout_rooted': '0',
# local files to ignore with status, addremove, ....
'exclude_glob': '.osc CVS .svn .* _linkerror *~ #*# *.orig *.bak *.changes.vctmp.*',
# whether to keep passwords in plaintext.
'plaintext_passwd': '1',
# whether to keep passwords in plaintext (deprecated (see creds manager)).
'plaintext_passwd': '0',
# limit the age of requests shown with 'osc req list'.
# this is a default only, can be overridden by 'osc req list -D NNN'
# Use 0 for unlimted.
@ -298,12 +296,6 @@ apiurl = %(apiurl)s
# local files to ignore with status, addremove, ....
#exclude_glob = %(exclude_glob)s
# keep passwords in plaintext.
# Set to 0 to obfuscate passwords. It's no real security, just
# prevents most people from remembering your password if they watch
# you editing this file.
#plaintext_passwd = %(plaintext_passwd)s
# limit the age of requests shown with 'osc req list'.
# this is a default only, can be overridden by 'osc req list -D NNN'
# Use 0 for unlimted.
@ -327,9 +319,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
@ -358,8 +347,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
@ -371,8 +358,6 @@ pass = %(pass)s
# User: mumblegack
# Plain text password
#pass =
# Force using of keyring for this API
#keyring = 1
"""
@ -672,7 +657,7 @@ def write_config(fname, cp):
raise
def config_set_option(section, opt, val=None, delete=False, update=True, **kwargs):
def config_set_option(section, opt, val=None, delete=False, update=True, creds_mgr_descr=None, **kwargs):
"""
Sets a config option. If val is not specified the current/default value is
returned. If val is specified, opt is set to val and the new value is returned.
@ -708,11 +693,37 @@ def config_set_option(section, opt, val=None, delete=False, update=True, **kwarg
raise oscerr.ConfigError('unknown config option \'%s\'' % opt, config['conffile'])
run = False
if val:
cp.set(section, opt, val)
write_config(config['conffile'], cp)
if opt == 'pass':
creds_mgr = _get_credentials_manager(section, cp)
user = _extract_user_compat(cp, section, creds_mgr)
old_pw = creds_mgr.get_password(section, user, defer=False)
try:
creds_mgr.delete_password(section, user)
if creds_mgr_descr:
creds_mgr_new = creds_mgr_descr.create(cp)
else:
creds_mgr_new = creds_mgr
creds_mgr_new.set_password(section, user, val)
write_config(config['conffile'], cp)
opt = credentials.AbstractCredentialsManager.config_entry
old_pw = None
finally:
if old_pw is not None:
creds_mgr.set_password(section, user, old_pw)
# not nice, but needed if the Credentials Manager will change
# something in cp
write_config(config['conffile'], cp)
else:
cp.set(section, opt, val)
write_config(config['conffile'], cp)
run = True
elif delete and cp.has_option(section, opt):
cp.remove_option(section, opt)
elif delete and (cp.has_option(section, opt) or opt == 'pass'):
if opt == 'pass':
creds_mgr = _get_credentials_manager(section, cp)
user = _extract_user_compar(cp, section, creds_mgr)
creds_mgr.delete_password(section, user)
else:
cp.remove_option(section, opt)
write_config(config['conffile'], cp)
run = True
if run and update:
@ -725,15 +736,17 @@ def config_set_option(section, opt, val=None, delete=False, update=True, **kwarg
return (opt, cp.get(section, opt, raw=True))
return (opt, None)
def passx_decode(passx):
"""decode the obfuscated password back to plain text password"""
return bz2.decompress(base64.b64decode(passx.encode("ascii"))).decode("ascii")
def _extract_user_compat(cp, section, creds_mgr):
"""
This extracts the user either from the ConfigParser or
the creds_mgr. Only needed for deprecated Gnome Keyring
"""
user = cp.get(section, 'user')
if user is None and hasattr(creds_mgr, 'get_user'):
user = creds_mgr.get_user(section)
return user
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' } ).
@ -742,37 +755,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'] = ''
if not config['plaintext_passwd']:
config['pass'] = ''
else:
config['passx'] = passx_encode(config['pass'])
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.
"""
@ -783,33 +778,34 @@ def add_section(filename, url, user, passwd):
except OscConfigParser.configparser.DuplicateSectionError:
# Section might have existed, but was empty
pass
cp.set(url, 'user', user)
if creds_mgr_descriptor:
creds_mgr = creds_mgr_descriptor.create(cp)
else:
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:
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')
return credentials.get_keyring_credentials_manager(cp)
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')
else:
cp.set(url, 'user', user)
if not config['plaintext_passwd']:
cp.remove_option(url, 'pass')
cp.set(url, 'passx', passx_encode(passwd))
else:
cp.remove_option(url, 'passx')
cp.set(url, 'pass', passwd)
write_config(filename, cp)
return credentials.GnomeKeyringCredentialsManager(cp, None)
elif cp.get(url, 'passx') is not None:
return credentials.ObfuscatedConfigFileCredentialsManager(cp, None)
return credentials.PlaintextConfigFileCredentialsManager(cp, None)
class APIHostOptionsEntry(dict):
def __getitem__(self, key, *args, **kwargs):
value = super(self.__class__, self).__getitem__(key, *args, **kwargs)
if key == 'pass' and callable(value):
value = value()
return value
def get_config(override_conffile=None,
@ -888,74 +884,16 @@ 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
creds_mgr = _get_credentials_manager(url, cp)
# if the deprecated gnomekeyring is used 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)
user = _extract_user_compat(cp, url, creds_mgr)
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'])
if config['plaintext_passwd'] and passwordx or not config['plaintext_passwd'] and password:
if config['plaintext_passwd']:
if password != passwordx:
print('%s: rewriting from encoded pass to plain pass' % url, file=sys.stderr)
add_section(conffile, url, user, passwordx)
password = passwordx
else:
if password != passwordx:
print('%s: rewriting from plain pass to encoded pass' % url, file=sys.stderr)
add_section(conffile, url, user, password)
if not config['plaintext_passwd']:
password = passwordx
raise oscerr.ConfigMissingCredentialsError('No user found in section %s' % url, conffile, url)
password = creds_mgr.get_password(url, user)
if password is None:
raise oscerr.ConfigMissingCredentialsError('No password found in section %s' % url, conffile, url)
if cp.has_option(url, 'http_headers'):
http_headers = cp.get(url, 'http_headers')
@ -972,9 +910,10 @@ def get_config(override_conffile=None,
raise oscerr.ConfigError(msg, conffile)
aliases[key] = url
api_host_options[apiurl] = {'user': user,
'pass': password,
'http_headers': http_headers}
entry = {'user': user,
'pass': password,
'http_headers': http_headers}
api_host_options[apiurl] = APIHostOptionsEntry(entry)
optional = ('realname', 'email', 'sslcertck', 'cafile', 'capath')
for key in optional:
@ -1015,6 +954,8 @@ def get_config(override_conffile=None,
if 'build_platform' in config:
print('Warning: Use of \'build_platform\' config option is deprecated! (use \'build_repository\' instead)', file=sys.stderr)
config['build_repository'] = config['build_platform']
if config['plaintext_passwd']:
print('The \'plaintext_passwd\' option is deprecated and will be ignored', file=sys.stderr)
config['verbose'] = int(config['verbose'])
# override values which we were called with
@ -1061,4 +1002,30 @@ def identify_conf():
return conffile
def interactive_config_setup(conffile, apiurl, initial=True):
user = raw_input('Username: ')
passwd = getpass.getpass()
creds_mgr_descr = select_credentials_manager_descr()
if initial:
config = {'user': user, 'pass': passwd}
if apiurl:
config['apiurl'] = apiurl
write_initial_config(conffile, config, creds_mgr_descriptor=creds_mgr_descr)
else:
add_section(conffile, apiurl, user, passwd, creds_mgr_descriptor=creds_mgr_descr)
def select_credentials_manager_descr():
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')
return creds_mgr_descriptors[i]
# vim: sw=4 et

View File

@ -55,7 +55,7 @@ try:
except ImportError:
from .util.helper import cmp_to_key
from osc.util.helper import decode_list, decode_it
from osc.util.helper import decode_list, decode_it, raw_input
try:
# python 2.6 and python 2.7
@ -7683,21 +7683,6 @@ def get_user_projpkgs(apiurl, user, role=None, exclude_projects=[], proj=True, p
filter_role(res, user, role)
return res
def raw_input(*args):
try:
import builtins
func = builtins.input
except ImportError:
#python 2.7
import __builtin__
func = __builtin__.raw_input
try:
return func(*args)
except EOFError:
# interpret ctrl-d as user abort
raise oscerr.UserAbort()
def run_external(filename, *args, **kwargs):
"""Executes the program filename via subprocess.call.

299
osc/credentials.py Normal file
View File

@ -0,0 +1,299 @@
import importlib
import bz2
import base64
import getpass
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 TransientCredentialsManager(AbstractCredentialsManager):
def __init__(self, *args, **kwargs):
super(self.__class__, self).__init__(*args, **kwargs)
self._password = None
def _process_options(self, options):
if options is not None:
raise RuntimeError('options must be None')
def get_password(self, url, user, defer=True):
if defer:
return self
return self()
def set_password(self, url, user, password):
self._password = password
self._cp.set(url, self.config_entry, self._qualified_name())
def delete_password(self, url, user):
self._password = None
def __call__(self):
if self._password is None:
self._password = getpass.getpass('Password: ')
return self._password
class TransientDescriptor(AbstractCredentialsManagerDescriptor):
def name(self):
return 'Transient password store'
def description(self):
return 'Do not store the password and always ask for the password'
def create(self, cp):
return TransientCredentialsManager(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())
descriptors.append(TransientDescriptor())
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

View File

@ -28,6 +28,11 @@ class ConfigMissingApiurl(ConfigError):
ConfigError.__init__(self, msg, fname)
self.url = url
class ConfigMissingCredentialsError(ConfigError):
def __init__(self, msg, fname, url):
ConfigError.__init__(self, msg, fname)
self.url = url
class APIError(OscBaseError):
"""Exception raised when there is an error in the output from the API"""
def __init__(self, msg):

View File

@ -66,4 +66,19 @@ def decode_it(obj):
return obj.decode(locale.getlocale()[1])
except:
return obj.decode('latin-1')
def raw_input(*args):
try:
import builtins
func = builtins.input
except ImportError:
#python 2.7
import __builtin__
func = __builtin__.raw_input
try:
return func(*args)
except EOFError:
# interpret ctrl-d as user abort
raise oscerr.UserAbort()

View File

@ -22,7 +22,6 @@ import test_project_status
import test_request
import test_setlinkrev
import test_prdiff
import test_conf
import test_results
import test_helpers
@ -41,7 +40,6 @@ suite.addTests(test_project_status.suite())
suite.addTests(test_request.suite())
suite.addTests(test_setlinkrev.suite())
suite.addTests(test_prdiff.suite())
suite.addTests(test_conf.suite())
suite.addTests(test_results.suite())
suite.addTests(test_helpers.suite())

View File

@ -1,32 +0,0 @@
from osc.conf import passx_encode, passx_decode
from common import OscTestCase
import os
FIXTURES_DIR = os.path.join(os.getcwd(), 'conf_fixtures')
def suite():
import unittest
return unittest.makeSuite(TestConf)
class TestConf(OscTestCase):
def _get_fixtures_dir(self):
return FIXTURES_DIR
def setUp(self):
return super(TestConf, self).setUp(copytree=False)
def testPassxEncodeDecode(self):
passwd = "J0e'sPassword!@#"
passx = passx_encode(passwd)
#base64.b64encode(passwd.encode('bz2'))
passx27 = "QlpoOTFBWSZTWaDg4dQAAAKfgCiAQABAEEAAJgCYgCAAMQAACEyYmTyei67AsYSDSaLuSKcKEhQcHDqA"
self.assertEqual(passwd, passx_decode(passx))
self.assertEqual(passwd, passx_decode(passx27))
self.assertEqual(passx, passx27)
if __name__ == '__main__':
import unittest
unittest.main()