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:
commit
e8d9fb52f6
@ -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))
|
||||
|
||||
|
279
osc/conf.py
279
osc/conf.py
@ -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
|
||||
|
17
osc/core.py
17
osc/core.py
@ -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
299
osc/credentials.py
Normal 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
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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())
|
||||
|
||||
|
@ -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()
|
Loading…
Reference in New Issue
Block a user