2019-08-27 15:08:38 +02:00
import base64
2022-07-28 12:28:33 +02:00
import bz2
2019-08-27 16:26:20 +02:00
import getpass
2022-07-28 12:28:33 +02:00
import importlib
2020-02-24 11:26:49 +01:00
import sys
2022-07-28 12:28:33 +02:00
from urllib . parse import urlsplit
2020-02-24 11:26:49 +01:00
2019-08-27 15:08:38 +02:00
try :
import keyring
except ImportError :
keyring = None
2020-02-24 11:26:49 +01:00
except BaseException as e :
# catch and report any exceptions raised in the 'keyring' module
msg = " Warning: Unable to load the ' keyring ' module due to an internal error: "
print ( msg , e , file = sys . stderr )
keyring = None
2022-03-24 14:35:43 +01:00
from . import conf
from . import oscerr
2019-08-27 15:08:38 +02:00
2022-07-28 19:11:29 +02:00
class AbstractCredentialsManagerDescriptor :
2019-08-27 15:08:38 +02:00
def name ( self ) :
raise NotImplementedError ( )
def description ( self ) :
raise NotImplementedError ( )
2022-03-24 11:31:01 +01:00
def priority ( self ) :
# priority determines order in the credentials managers list
# higher number means higher priority
raise NotImplementedError ( )
2019-08-27 15:08:38 +02:00
def create ( self , cp ) :
raise NotImplementedError ( )
def __lt__ ( self , other ) :
2022-03-24 11:31:01 +01:00
return ( - self . priority ( ) , self . name ( ) ) < ( - other . priority ( ) , other . name ( ) )
2019-08-27 15:08:38 +02:00
2022-07-28 19:11:29 +02:00
class AbstractCredentialsManager :
2019-08-27 15:08:38 +02:00
config_entry = ' credentials_mgr_class '
def __init__ ( self , cp , options ) :
2022-07-28 19:11:29 +02:00
super ( ) . __init__ ( )
2019-08-27 15:08:38 +02:00
self . _cp = cp
self . _process_options ( options )
2019-10-16 10:41:06 +02:00
@classmethod
def create ( cls , cp , options ) :
return cls ( cp , options )
2022-09-07 16:10:55 +02:00
def _get_password ( self , url , user , apiurl = None ) :
2019-08-27 15:08:38 +02:00
raise NotImplementedError ( )
2022-09-07 16:10:55 +02:00
def get_password ( self , url , user , defer = True , apiurl = None ) :
2022-04-04 10:15:35 +02:00
if defer :
2023-08-22 15:34:45 +02:00
return conf . Password ( lambda : self . _get_password ( url , user , apiurl = apiurl ) )
2022-04-04 10:15:35 +02:00
else :
2023-08-22 15:34:45 +02:00
return conf . Password ( self . _get_password ( url , user , apiurl = apiurl ) )
2022-04-04 10:15:35 +02:00
2019-08-27 15:08:38 +02:00
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 ) :
2022-09-07 16:10:55 +02:00
def get_password ( self , url , user , defer = True , apiurl = None ) :
2024-01-04 16:39:27 +01:00
password = self . _cp . get ( url , " pass " , fallback = None , raw = True )
if password is None :
return None
return conf . Password ( password )
2019-08-27 15:08:38 +02:00
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 ) :
2022-03-24 11:22:21 +01:00
return ' Config '
2019-08-27 15:08:38 +02:00
def description ( self ) :
2022-03-24 11:22:21 +01:00
return ' Store the password in plain text in the osc config file [insecure, persistent] '
2019-08-27 15:08:38 +02:00
2022-03-24 11:31:01 +01:00
def priority ( self ) :
return 1
2019-08-27 15:08:38 +02:00
def create ( self , cp ) :
return PlaintextConfigFileCredentialsManager ( cp , None )
2022-09-07 16:10:55 +02:00
class ObfuscatedConfigFileCredentialsManager ( PlaintextConfigFileCredentialsManager ) :
def get_password ( self , url , user , defer = True , apiurl = None ) :
2019-11-04 14:25:48 +01:00
if self . _cp . has_option ( url , ' passx ' , proper = True ) :
passwd = self . _cp . get ( url , ' passx ' , raw = True )
else :
2022-08-29 09:19:07 +02:00
passwd = super ( ) . get_password ( url , user , apiurl = apiurl )
2024-01-04 16:39:27 +01:00
password = self . decode_password ( passwd )
return conf . Password ( password )
2019-08-27 15:08:38 +02:00
def set_password ( self , url , user , password ) :
compressed_pw = bz2 . compress ( password . encode ( ' ascii ' ) )
password = base64 . b64encode ( compressed_pw ) . decode ( " ascii " )
2022-08-29 09:19:07 +02:00
super ( ) . set_password ( url , user , password )
2019-08-27 15:08:38 +02:00
2019-10-29 11:04:22 +01:00
def delete_password ( self , url , user ) :
self . _cp . remove_option ( url , ' passx ' )
2022-08-29 09:19:07 +02:00
super ( ) . delete_password ( url , user )
2019-10-29 11:04:22 +01:00
2019-08-27 15:08:38 +02:00
@classmethod
def decode_password ( cls , password ) :
2022-07-26 15:07:40 +02:00
if password is None :
# avoid crash on encoding None when 'pass' is not specified in the config
return None
2019-08-27 15:08:38 +02:00
compressed_pw = base64 . b64decode ( password . encode ( " ascii " ) )
return bz2 . decompress ( compressed_pw ) . decode ( " ascii " )
class ObfuscatedConfigFileDescriptor ( AbstractCredentialsManagerDescriptor ) :
def name ( self ) :
2022-03-24 11:22:21 +01:00
return ' Obfuscated config '
2019-08-27 15:08:38 +02:00
def description ( self ) :
2022-03-24 11:22:21 +01:00
return ' Store the password in obfuscated form in the osc config file [insecure, persistent] '
2019-08-27 15:08:38 +02:00
2022-03-24 11:31:01 +01:00
def priority ( self ) :
return 2
2019-08-27 15:08:38 +02:00
def create ( self , cp ) :
return ObfuscatedConfigFileCredentialsManager ( cp , None )
2019-08-27 16:26:20 +02:00
class TransientCredentialsManager ( AbstractCredentialsManager ) :
def __init__ ( self , * args , * * kwargs ) :
2022-08-29 09:19:07 +02:00
super ( ) . __init__ ( * args , * * kwargs )
2019-08-27 16:26:20 +02:00
self . _password = None
def _process_options ( self , options ) :
if options is not None :
raise RuntimeError ( ' options must be None ' )
2022-09-07 16:10:55 +02:00
def _get_password ( self , url , user , apiurl = None ) :
2022-04-04 10:15:35 +02:00
if self . _password is None :
2022-09-07 16:10:55 +02:00
if apiurl :
# strip scheme from apiurl because we don't want to display it to the user
apiurl_no_scheme = urlsplit ( apiurl ) [ 1 ]
msg = f ' Password [ { user } @ { apiurl_no_scheme } ]: '
else :
msg = ' Password: '
self . _password = getpass . getpass ( msg )
2022-04-04 10:15:35 +02:00
return self . _password
2019-08-27 16:26:20 +02:00
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
class TransientDescriptor ( AbstractCredentialsManagerDescriptor ) :
def name ( self ) :
2022-03-24 11:22:21 +01:00
return ' Transient '
2019-08-27 16:26:20 +02:00
def description ( self ) :
2022-03-24 11:22:21 +01:00
return ' Do not store the password and always ask for it [secure, in-memory] '
2019-08-27 16:26:20 +02:00
2022-03-24 11:31:01 +01:00
def priority ( self ) :
return 3
2019-08-27 16:26:20 +02:00
def create ( self , cp ) :
return TransientCredentialsManager ( cp , None )
2019-08-27 15:08:38 +02:00
class KeyringCredentialsManager ( AbstractCredentialsManager ) :
2024-04-08 09:21:15 +02:00
def __init__ ( self , * args , * * kwargs ) :
super ( ) . __init__ ( * args , * * kwargs )
self . _password = None
2019-08-27 15:08:38 +02:00
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 ) :
2022-03-24 14:35:43 +01:00
try :
keyring_backend = keyring . core . load_keyring ( self . _backend_cls_name )
except ModuleNotFoundError :
2022-07-28 19:11:29 +02:00
msg = f " Invalid credentials_mgr_class: { self . _backend_cls_name } "
2022-03-24 14:35:43 +01:00
raise oscerr . ConfigError ( msg , conf . config [ ' conffile ' ] )
2019-08-27 15:08:38 +02:00
keyring . set_keyring ( keyring_backend )
2019-10-16 10:41:06 +02:00
@classmethod
def create ( cls , cp , options ) :
if not has_keyring_support ( ) :
return None
2022-08-29 09:19:07 +02:00
return super ( ) . create ( cp , options )
2019-10-16 10:41:06 +02:00
2022-09-07 16:10:55 +02:00
def _get_password ( self , url , user , apiurl = None ) :
2024-04-08 09:21:15 +02:00
if self . _password is None :
self . _load_backend ( )
self . _password = keyring . get_password ( urlsplit ( url ) [ 1 ] , user )
# TODO: this works fine on the command-line but a long-running process using osc library would start failing after changing the password in the keyring
# TODO: implement retrieving the password again after basic auth fails; sufficiently inform user about what's being done
return self . _password
2019-08-27 15:08:38 +02:00
def set_password ( self , url , user , password ) :
self . _load_backend ( )
2019-11-03 12:33:17 +01:00
keyring . set_password ( urlsplit ( url ) [ 1 ] , user , password )
2024-01-06 09:54:57 +01:00
config_value = f " { self . _qualified_name ( ) } : { self . _backend_cls_name } "
2019-08-27 15:08:38 +02:00
self . _cp . set ( url , self . config_entry , config_value )
2024-04-08 09:21:15 +02:00
self . _password = password
2019-08-27 15:08:38 +02:00
def delete_password ( self , url , user ) :
self . _load_backend ( )
2022-11-01 19:40:39 +01:00
service = urlsplit ( url ) [ 1 ]
data = keyring . get_password ( service , user )
if data is None :
return
keyring . delete_password ( service , user )
2019-08-27 15:08:38 +02:00
class KeyringCredentialsDescriptor ( AbstractCredentialsManagerDescriptor ) :
2022-03-24 11:31:01 +01:00
def __init__ ( self , keyring_backend , name = None , description = None , priority = None ) :
2019-08-27 15:08:38 +02:00
self . _keyring_backend = keyring_backend
2022-03-24 11:11:36 +01:00
self . _name = name
self . _description = description
2022-03-24 11:31:01 +01:00
self . _priority = priority
2019-08-27 15:08:38 +02:00
def name ( self ) :
2022-03-24 11:11:36 +01:00
if self . _name :
return self . _name
2020-02-14 09:35:07 +01:00
if hasattr ( self . _keyring_backend , ' name ' ) :
return self . _keyring_backend . name
2022-03-24 11:11:36 +01:00
return self . _keyring_backend . __class__ . __name__
2019-08-27 15:08:38 +02:00
def description ( self ) :
2022-03-24 11:11:36 +01:00
if self . _description :
return self . _description
2019-08-27 15:08:38 +02:00
return ' Backend provided by python-keyring '
2022-03-24 11:31:01 +01:00
def priority ( self ) :
if self . _priority is not None :
return self . _priority
return 0
2019-08-27 15:08:38 +02:00
def create ( self , cp ) :
qualified_backend_name = qualified_name ( self . _keyring_backend )
return KeyringCredentialsManager ( cp , qualified_backend_name )
2022-03-24 11:11:36 +01:00
# we're supporting only selected python-keyring backends in osc
SUPPORTED_KEYRING_BACKENDS = {
" keyutils.osc.OscKernelKeyringBackend " : {
" name " : " Kernel keyring " ,
" description " : " Store password in user session keyring in kernel keyring [secure, in-memory, per-session] " ,
2022-03-24 11:31:01 +01:00
" priority " : 10 ,
2022-03-24 11:11:36 +01:00
} ,
" keyring.backends.SecretService.Keyring " : {
" name " : " Secret Service " ,
" description " : " Store password in Secret Service (GNOME Keyring backend) [secure, persistent] " ,
2022-03-24 11:31:01 +01:00
" priority " : 9 ,
2022-03-24 11:11:36 +01:00
} ,
" keyring.backends.kwallet.DBusKeyring " : {
" name " : " KWallet " ,
" description " : " Store password in KWallet [secure, persistent] " ,
2022-03-24 11:31:01 +01:00
" priority " : 8 ,
2022-03-24 11:11:36 +01:00
} ,
}
2019-08-27 15:08:38 +02:00
def get_credentials_manager_descriptors ( ) :
descriptors = [ ]
2022-03-24 11:11:36 +01:00
if has_keyring_support ( ) :
for backend in keyring . backend . get_all_keyring ( ) :
qualified_backend_name = qualified_name ( backend )
data = SUPPORTED_KEYRING_BACKENDS . get ( qualified_backend_name , None )
if not data :
continue
2022-03-24 11:31:01 +01:00
descriptor = KeyringCredentialsDescriptor (
backend ,
data [ " name " ] ,
data [ " description " ] ,
data [ " priority " ]
)
2022-03-24 11:11:36 +01:00
descriptors . append ( descriptor )
2019-08-27 15:08:38 +02:00
descriptors . append ( PlaintextConfigFileDescriptor ( ) )
descriptors . append ( ObfuscatedConfigFileDescriptor ( ) )
2019-08-27 16:26:20 +02:00
descriptors . append ( TransientDescriptor ( ) )
2022-03-24 11:31:01 +01:00
descriptors . sort ( )
2019-08-27 15:08:38 +02:00
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 )
2022-06-21 08:33:38 +02:00
try :
creds_mgr = getattr ( importlib . import_module ( mod ) , cls ) . create ( cp , options )
except ModuleNotFoundError :
2022-07-28 19:11:29 +02:00
msg = f " Invalid credentials_mgr_class: { creds_mgr_cls } "
2022-06-21 08:33:38 +02:00
raise oscerr . ConfigError ( msg , conf . config [ ' conffile ' ] )
return creds_mgr
2019-08-27 15:08:38 +02:00
def qualified_name ( obj ) :
2024-01-06 09:54:57 +01:00
return f " { obj . __module__ } . { obj . __class__ . __name__ } "
2019-08-27 15:08:38 +02:00
def has_keyring_support ( ) :
return keyring is not None