PageRenderTime 50ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/osc/credentials.py

https://github.com/openSUSE/osc
Python | 429 lines | 326 code | 98 blank | 5 comment | 45 complexity | 60ca1e761fd29480d4eb3050bab525c0 MD5 | raw file
  1. import importlib
  2. import bz2
  3. import base64
  4. import getpass
  5. import sys
  6. try:
  7. from urllib.parse import urlsplit
  8. except ImportError:
  9. from urlparse import urlsplit
  10. try:
  11. import keyring
  12. except ImportError:
  13. keyring = None
  14. except BaseException as e:
  15. # catch and report any exceptions raised in the 'keyring' module
  16. msg = "Warning: Unable to load the 'keyring' module due to an internal error:"
  17. print(msg, e, file=sys.stderr)
  18. keyring = None
  19. try:
  20. import gnomekeyring
  21. except ImportError:
  22. gnomekeyring = None
  23. except BaseException as e:
  24. # catch and report any exceptions raised in the 'gnomekeyring' module
  25. msg = "Warning: Unable to load the 'gnomekeyring' module due to an internal error:"
  26. print(msg, e, file=sys.stderr)
  27. gnomekeyring = None
  28. from . import conf
  29. from . import oscerr
  30. class _LazyPassword(object):
  31. def __init__(self, pwfunc):
  32. self._pwfunc = pwfunc
  33. self._password = None
  34. def __str__(self):
  35. if self._password is None:
  36. password = self._pwfunc()
  37. if callable(password):
  38. print('Warning: use of a deprecated credentials manager API.',
  39. file=sys.stderr)
  40. password = password()
  41. if password is None:
  42. raise oscerr.OscIOError(None, 'Unable to retrieve password')
  43. self._password = password
  44. return self._password
  45. def __len__(self):
  46. return len(str(self))
  47. def __add__(self, other):
  48. return str(self) + other
  49. def __radd__(self, other):
  50. return other + str(self)
  51. def __getattr__(self, name):
  52. return getattr(str(self), name)
  53. class AbstractCredentialsManagerDescriptor(object):
  54. def name(self):
  55. raise NotImplementedError()
  56. def description(self):
  57. raise NotImplementedError()
  58. def priority(self):
  59. # priority determines order in the credentials managers list
  60. # higher number means higher priority
  61. raise NotImplementedError()
  62. def create(self, cp):
  63. raise NotImplementedError()
  64. def __lt__(self, other):
  65. return (-self.priority(), self.name()) < (-other.priority(), other.name())
  66. class AbstractCredentialsManager(object):
  67. config_entry = 'credentials_mgr_class'
  68. def __init__(self, cp, options):
  69. super(AbstractCredentialsManager, self).__init__()
  70. self._cp = cp
  71. self._process_options(options)
  72. @classmethod
  73. def create(cls, cp, options):
  74. return cls(cp, options)
  75. def _get_password(self, url, user):
  76. raise NotImplementedError()
  77. def get_password(self, url, user, defer=True):
  78. if defer:
  79. return _LazyPassword(lambda: self._get_password(url, user))
  80. else:
  81. return self._get_password(url, user)
  82. def set_password(self, url, user, password):
  83. raise NotImplementedError()
  84. def delete_password(self, url, user):
  85. raise NotImplementedError()
  86. def _qualified_name(self):
  87. return qualified_name(self)
  88. def _process_options(self, options):
  89. pass
  90. class PlaintextConfigFileCredentialsManager(AbstractCredentialsManager):
  91. def get_password(self, url, user, defer=True):
  92. return self._cp.get(url, 'pass', raw=True)
  93. def set_password(self, url, user, password):
  94. self._cp.set(url, 'pass', password)
  95. self._cp.set(url, self.config_entry, self._qualified_name())
  96. def delete_password(self, url, user):
  97. self._cp.remove_option(url, 'pass')
  98. def _process_options(self, options):
  99. if options is not None:
  100. raise RuntimeError('options must be None')
  101. class PlaintextConfigFileDescriptor(AbstractCredentialsManagerDescriptor):
  102. def name(self):
  103. return 'Config'
  104. def description(self):
  105. return 'Store the password in plain text in the osc config file [insecure, persistent]'
  106. def priority(self):
  107. return 1
  108. def create(self, cp):
  109. return PlaintextConfigFileCredentialsManager(cp, None)
  110. class ObfuscatedConfigFileCredentialsManager(
  111. PlaintextConfigFileCredentialsManager):
  112. def get_password(self, url, user, defer=True):
  113. if self._cp.has_option(url, 'passx', proper=True):
  114. passwd = self._cp.get(url, 'passx', raw=True)
  115. else:
  116. passwd = super(self.__class__, self).get_password(url, user)
  117. return self.decode_password(passwd)
  118. def set_password(self, url, user, password):
  119. compressed_pw = bz2.compress(password.encode('ascii'))
  120. password = base64.b64encode(compressed_pw).decode("ascii")
  121. super(self.__class__, self).set_password(url, user, password)
  122. def delete_password(self, url, user):
  123. self._cp.remove_option(url, 'passx')
  124. super(self.__class__, self).delete_password(url, user)
  125. @classmethod
  126. def decode_password(cls, password):
  127. compressed_pw = base64.b64decode(password.encode("ascii"))
  128. return bz2.decompress(compressed_pw).decode("ascii")
  129. class ObfuscatedConfigFileDescriptor(AbstractCredentialsManagerDescriptor):
  130. def name(self):
  131. return 'Obfuscated config'
  132. def description(self):
  133. return 'Store the password in obfuscated form in the osc config file [insecure, persistent]'
  134. def priority(self):
  135. return 2
  136. def create(self, cp):
  137. return ObfuscatedConfigFileCredentialsManager(cp, None)
  138. class TransientCredentialsManager(AbstractCredentialsManager):
  139. def __init__(self, *args, **kwargs):
  140. super(self.__class__, self).__init__(*args, **kwargs)
  141. self._password = None
  142. def _process_options(self, options):
  143. if options is not None:
  144. raise RuntimeError('options must be None')
  145. def _get_password(self, url, user):
  146. if self._password is None:
  147. self._password = getpass.getpass('Password: ')
  148. return self._password
  149. def set_password(self, url, user, password):
  150. self._password = password
  151. self._cp.set(url, self.config_entry, self._qualified_name())
  152. def delete_password(self, url, user):
  153. self._password = None
  154. class TransientDescriptor(AbstractCredentialsManagerDescriptor):
  155. def name(self):
  156. return 'Transient'
  157. def description(self):
  158. return 'Do not store the password and always ask for it [secure, in-memory]'
  159. def priority(self):
  160. return 3
  161. def create(self, cp):
  162. return TransientCredentialsManager(cp, None)
  163. class KeyringCredentialsManager(AbstractCredentialsManager):
  164. def _process_options(self, options):
  165. if options is None:
  166. raise RuntimeError('options may not be None')
  167. self._backend_cls_name = options
  168. def _load_backend(self):
  169. try:
  170. keyring_backend = keyring.core.load_keyring(self._backend_cls_name)
  171. except ModuleNotFoundError:
  172. msg = "Invalid credentials_mgr_class: {}".format(self._backend_cls_name)
  173. raise oscerr.ConfigError(msg, conf.config['conffile'])
  174. keyring.set_keyring(keyring_backend)
  175. @classmethod
  176. def create(cls, cp, options):
  177. if not has_keyring_support():
  178. return None
  179. return super(cls, cls).create(cp, options)
  180. def _get_password(self, url, user):
  181. self._load_backend()
  182. return keyring.get_password(urlsplit(url)[1], user)
  183. def set_password(self, url, user, password):
  184. self._load_backend()
  185. keyring.set_password(urlsplit(url)[1], user, password)
  186. config_value = self._qualified_name() + ':' + self._backend_cls_name
  187. self._cp.set(url, self.config_entry, config_value)
  188. def delete_password(self, url, user):
  189. self._load_backend()
  190. keyring.delete_password(urlsplit(url)[1], user)
  191. class KeyringCredentialsDescriptor(AbstractCredentialsManagerDescriptor):
  192. def __init__(self, keyring_backend, name=None, description=None, priority=None):
  193. self._keyring_backend = keyring_backend
  194. self._name = name
  195. self._description = description
  196. self._priority = priority
  197. def name(self):
  198. if self._name:
  199. return self._name
  200. if hasattr(self._keyring_backend, 'name'):
  201. return self._keyring_backend.name
  202. return self._keyring_backend.__class__.__name__
  203. def description(self):
  204. if self._description:
  205. return self._description
  206. return 'Backend provided by python-keyring'
  207. def priority(self):
  208. if self._priority is not None:
  209. return self._priority
  210. return 0
  211. def create(self, cp):
  212. qualified_backend_name = qualified_name(self._keyring_backend)
  213. return KeyringCredentialsManager(cp, qualified_backend_name)
  214. class GnomeKeyringCredentialsManager(AbstractCredentialsManager):
  215. @classmethod
  216. def create(cls, cp, options):
  217. if gnomekeyring is None:
  218. return None
  219. return super(cls, cls).create(cp, options)
  220. def _get_password(self, url, user):
  221. gk_data = self._keyring_data(url, user)
  222. if gk_data is None:
  223. return None
  224. return gk_data['password']
  225. def set_password(self, url, user, password):
  226. scheme, host, path = self._urlsplit(url)
  227. gnomekeyring.set_network_password_sync(
  228. user=user,
  229. password=password,
  230. protocol=scheme,
  231. server=host,
  232. object=path)
  233. self._cp.set(url, self.config_entry, self._qualified_name())
  234. def delete_password(self, url, user):
  235. gk_data = self._keyring_data(url, user)
  236. if gk_data is None:
  237. return
  238. gnomekeyring.item_delete_sync(gk_data['keyring'], gk_data['item_id'])
  239. def get_user(self, url):
  240. gk_data = self._keyring_data(url, None)
  241. if gk_data is None:
  242. return None
  243. return gk_data['user']
  244. def _keyring_data(self, url, user):
  245. scheme, host, path = self._urlsplit(url)
  246. try:
  247. entries = gnomekeyring.find_network_password_sync(protocol=scheme,
  248. server=host,
  249. object=path)
  250. except gnomekeyring.NoMatchError:
  251. return None
  252. for entry in entries:
  253. if 'user' not in entry or 'password' not in entry:
  254. continue
  255. if user is None or entry['user'] == user:
  256. return entry
  257. return None
  258. def _urlsplit(self, url):
  259. splitted_url = urlsplit(url)
  260. return splitted_url.scheme, splitted_url.netloc, splitted_url.path
  261. class GnomeKeyringCredentialsDescriptor(AbstractCredentialsManagerDescriptor):
  262. def name(self):
  263. return 'GNOME Keyring Manager (deprecated)'
  264. def description(self):
  265. return 'Deprecated GNOME Keyring Manager. If you use \
  266. this we will send you a Dial-In modem'
  267. def priority(self):
  268. return 0
  269. def create(self, cp):
  270. return GnomeKeyringCredentialsManager(cp, None)
  271. # we're supporting only selected python-keyring backends in osc
  272. SUPPORTED_KEYRING_BACKENDS = {
  273. "keyutils.osc.OscKernelKeyringBackend": {
  274. "name": "Kernel keyring",
  275. "description": "Store password in user session keyring in kernel keyring [secure, in-memory, per-session]",
  276. "priority": 10,
  277. },
  278. "keyring.backends.SecretService.Keyring": {
  279. "name": "Secret Service",
  280. "description": "Store password in Secret Service (GNOME Keyring backend) [secure, persistent]",
  281. "priority": 9,
  282. },
  283. "keyring.backends.kwallet.DBusKeyring": {
  284. "name": "KWallet",
  285. "description": "Store password in KWallet [secure, persistent]",
  286. "priority": 8,
  287. },
  288. }
  289. def get_credentials_manager_descriptors():
  290. descriptors = []
  291. if has_keyring_support():
  292. for backend in keyring.backend.get_all_keyring():
  293. qualified_backend_name = qualified_name(backend)
  294. data = SUPPORTED_KEYRING_BACKENDS.get(qualified_backend_name, None)
  295. if not data:
  296. continue
  297. descriptor = KeyringCredentialsDescriptor(
  298. backend,
  299. data["name"],
  300. data["description"],
  301. data["priority"]
  302. )
  303. descriptors.append(descriptor)
  304. if gnomekeyring:
  305. descriptors.append(GnomeKeyringCredentialsDescriptor())
  306. descriptors.append(PlaintextConfigFileDescriptor())
  307. descriptors.append(ObfuscatedConfigFileDescriptor())
  308. descriptors.append(TransientDescriptor())
  309. descriptors.sort()
  310. return descriptors
  311. def get_keyring_credentials_manager(cp):
  312. keyring_backend = keyring.get_keyring()
  313. return KeyringCredentialsManager(cp, qualified_name(keyring_backend))
  314. def create_credentials_manager(url, cp):
  315. config_entry = cp.get(url, AbstractCredentialsManager.config_entry)
  316. if ':' in config_entry:
  317. creds_mgr_cls, options = config_entry.split(':', 1)
  318. else:
  319. creds_mgr_cls = config_entry
  320. options = None
  321. mod, cls = creds_mgr_cls.rsplit('.', 1)
  322. try:
  323. creds_mgr = getattr(importlib.import_module(mod), cls).create(cp, options)
  324. except ModuleNotFoundError:
  325. msg = "Invalid credentials_mgr_class: {}".format(creds_mgr_cls)
  326. raise oscerr.ConfigError(msg, conf.config['conffile'])
  327. return creds_mgr
  328. def qualified_name(obj):
  329. return obj.__module__ + '.' + obj.__class__.__name__
  330. def has_keyring_support():
  331. return keyring is not None