You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

484 lines
22 KiB

4 years ago
  1. # Copyright (c) 2010 Mitch Garnaat http://garnaat.org/
  2. # Copyright 2010 Google Inc.
  3. # Copyright (c) 2010, Eucalyptus Systems, Inc.
  4. # Copyright (c) 2011, Nexenta Systems Inc.
  5. # All rights reserved.
  6. #
  7. # Permission is hereby granted, free of charge, to any person obtaining a
  8. # copy of this software and associated documentation files (the
  9. # "Software"), to deal in the Software without restriction, including
  10. # without limitation the rights to use, copy, modify, merge, publish, dis-
  11. # tribute, sublicense, and/or sell copies of the Software, and to permit
  12. # persons to whom the Software is furnished to do so, subject to the fol-
  13. # lowing conditions:
  14. #
  15. # The above copyright notice and this permission notice shall be included
  16. # in all copies or substantial portions of the Software.
  17. #
  18. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  19. # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
  20. # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
  21. # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  22. # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  23. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
  24. # IN THE SOFTWARE.
  25. """
  26. This class encapsulates the provider-specific header differences.
  27. """
  28. import os
  29. from boto.compat import six
  30. from datetime import datetime
  31. import boto
  32. from boto import config
  33. from boto.compat import expanduser
  34. from boto.pyami.config import Config
  35. from boto.exception import InvalidInstanceMetadataError
  36. from boto.gs.acl import ACL
  37. from boto.gs.acl import CannedACLStrings as CannedGSACLStrings
  38. from boto.s3.acl import CannedACLStrings as CannedS3ACLStrings
  39. from boto.s3.acl import Policy
  40. HEADER_PREFIX_KEY = 'header_prefix'
  41. METADATA_PREFIX_KEY = 'metadata_prefix'
  42. AWS_HEADER_PREFIX = 'x-amz-'
  43. GOOG_HEADER_PREFIX = 'x-goog-'
  44. ACL_HEADER_KEY = 'acl-header'
  45. AUTH_HEADER_KEY = 'auth-header'
  46. COPY_SOURCE_HEADER_KEY = 'copy-source-header'
  47. COPY_SOURCE_VERSION_ID_HEADER_KEY = 'copy-source-version-id-header'
  48. COPY_SOURCE_RANGE_HEADER_KEY = 'copy-source-range-header'
  49. DELETE_MARKER_HEADER_KEY = 'delete-marker-header'
  50. DATE_HEADER_KEY = 'date-header'
  51. METADATA_DIRECTIVE_HEADER_KEY = 'metadata-directive-header'
  52. RESUMABLE_UPLOAD_HEADER_KEY = 'resumable-upload-header'
  53. SECURITY_TOKEN_HEADER_KEY = 'security-token-header'
  54. STORAGE_CLASS_HEADER_KEY = 'storage-class'
  55. MFA_HEADER_KEY = 'mfa-header'
  56. SERVER_SIDE_ENCRYPTION_KEY = 'server-side-encryption-header'
  57. VERSION_ID_HEADER_KEY = 'version-id-header'
  58. RESTORE_HEADER_KEY = 'restore-header'
  59. STORAGE_COPY_ERROR = 'StorageCopyError'
  60. STORAGE_CREATE_ERROR = 'StorageCreateError'
  61. STORAGE_DATA_ERROR = 'StorageDataError'
  62. STORAGE_PERMISSIONS_ERROR = 'StoragePermissionsError'
  63. STORAGE_RESPONSE_ERROR = 'StorageResponseError'
  64. NO_CREDENTIALS_PROVIDED = object()
  65. class ProfileNotFoundError(ValueError):
  66. pass
  67. class Provider(object):
  68. CredentialMap = {
  69. 'aws': ('aws_access_key_id', 'aws_secret_access_key',
  70. 'aws_security_token', 'aws_profile'),
  71. 'google': ('gs_access_key_id', 'gs_secret_access_key',
  72. None, None),
  73. }
  74. AclClassMap = {
  75. 'aws': Policy,
  76. 'google': ACL
  77. }
  78. CannedAclsMap = {
  79. 'aws': CannedS3ACLStrings,
  80. 'google': CannedGSACLStrings
  81. }
  82. HostKeyMap = {
  83. 'aws': 's3',
  84. 'google': 'gs'
  85. }
  86. ChunkedTransferSupport = {
  87. 'aws': False,
  88. 'google': True
  89. }
  90. MetadataServiceSupport = {
  91. 'aws': True,
  92. 'google': False
  93. }
  94. # If you update this map please make sure to put "None" for the
  95. # right-hand-side for any headers that don't apply to a provider, rather
  96. # than simply leaving that header out (which would cause KeyErrors).
  97. HeaderInfoMap = {
  98. 'aws': {
  99. HEADER_PREFIX_KEY: AWS_HEADER_PREFIX,
  100. METADATA_PREFIX_KEY: AWS_HEADER_PREFIX + 'meta-',
  101. ACL_HEADER_KEY: AWS_HEADER_PREFIX + 'acl',
  102. AUTH_HEADER_KEY: 'AWS',
  103. COPY_SOURCE_HEADER_KEY: AWS_HEADER_PREFIX + 'copy-source',
  104. COPY_SOURCE_VERSION_ID_HEADER_KEY: AWS_HEADER_PREFIX +
  105. 'copy-source-version-id',
  106. COPY_SOURCE_RANGE_HEADER_KEY: AWS_HEADER_PREFIX +
  107. 'copy-source-range',
  108. DATE_HEADER_KEY: AWS_HEADER_PREFIX + 'date',
  109. DELETE_MARKER_HEADER_KEY: AWS_HEADER_PREFIX + 'delete-marker',
  110. METADATA_DIRECTIVE_HEADER_KEY: AWS_HEADER_PREFIX +
  111. 'metadata-directive',
  112. RESUMABLE_UPLOAD_HEADER_KEY: None,
  113. SECURITY_TOKEN_HEADER_KEY: AWS_HEADER_PREFIX + 'security-token',
  114. SERVER_SIDE_ENCRYPTION_KEY: AWS_HEADER_PREFIX +
  115. 'server-side-encryption',
  116. VERSION_ID_HEADER_KEY: AWS_HEADER_PREFIX + 'version-id',
  117. STORAGE_CLASS_HEADER_KEY: AWS_HEADER_PREFIX + 'storage-class',
  118. MFA_HEADER_KEY: AWS_HEADER_PREFIX + 'mfa',
  119. RESTORE_HEADER_KEY: AWS_HEADER_PREFIX + 'restore',
  120. },
  121. 'google': {
  122. HEADER_PREFIX_KEY: GOOG_HEADER_PREFIX,
  123. METADATA_PREFIX_KEY: GOOG_HEADER_PREFIX + 'meta-',
  124. ACL_HEADER_KEY: GOOG_HEADER_PREFIX + 'acl',
  125. AUTH_HEADER_KEY: 'GOOG1',
  126. COPY_SOURCE_HEADER_KEY: GOOG_HEADER_PREFIX + 'copy-source',
  127. COPY_SOURCE_VERSION_ID_HEADER_KEY: GOOG_HEADER_PREFIX +
  128. 'copy-source-version-id',
  129. COPY_SOURCE_RANGE_HEADER_KEY: None,
  130. DATE_HEADER_KEY: GOOG_HEADER_PREFIX + 'date',
  131. DELETE_MARKER_HEADER_KEY: GOOG_HEADER_PREFIX + 'delete-marker',
  132. METADATA_DIRECTIVE_HEADER_KEY: GOOG_HEADER_PREFIX +
  133. 'metadata-directive',
  134. RESUMABLE_UPLOAD_HEADER_KEY: GOOG_HEADER_PREFIX + 'resumable',
  135. SECURITY_TOKEN_HEADER_KEY: GOOG_HEADER_PREFIX + 'security-token',
  136. SERVER_SIDE_ENCRYPTION_KEY: None,
  137. # Note that this version header is not to be confused with
  138. # the Google Cloud Storage 'x-goog-api-version' header.
  139. VERSION_ID_HEADER_KEY: GOOG_HEADER_PREFIX + 'version-id',
  140. STORAGE_CLASS_HEADER_KEY: GOOG_HEADER_PREFIX + 'storage-class',
  141. MFA_HEADER_KEY: None,
  142. RESTORE_HEADER_KEY: None,
  143. }
  144. }
  145. ErrorMap = {
  146. 'aws': {
  147. STORAGE_COPY_ERROR: boto.exception.S3CopyError,
  148. STORAGE_CREATE_ERROR: boto.exception.S3CreateError,
  149. STORAGE_DATA_ERROR: boto.exception.S3DataError,
  150. STORAGE_PERMISSIONS_ERROR: boto.exception.S3PermissionsError,
  151. STORAGE_RESPONSE_ERROR: boto.exception.S3ResponseError,
  152. },
  153. 'google': {
  154. STORAGE_COPY_ERROR: boto.exception.GSCopyError,
  155. STORAGE_CREATE_ERROR: boto.exception.GSCreateError,
  156. STORAGE_DATA_ERROR: boto.exception.GSDataError,
  157. STORAGE_PERMISSIONS_ERROR: boto.exception.GSPermissionsError,
  158. STORAGE_RESPONSE_ERROR: boto.exception.GSResponseError,
  159. }
  160. }
  161. def __init__(self, name, access_key=None, secret_key=None,
  162. security_token=None, profile_name=None):
  163. self.host = None
  164. self.port = None
  165. self.host_header = None
  166. self.access_key = access_key
  167. self.secret_key = secret_key
  168. self.security_token = security_token
  169. self.profile_name = profile_name
  170. self.name = name
  171. self.acl_class = self.AclClassMap[self.name]
  172. self.canned_acls = self.CannedAclsMap[self.name]
  173. self._credential_expiry_time = None
  174. # Load shared credentials file if it exists
  175. shared_path = os.path.join(expanduser('~'), '.' + name, 'credentials')
  176. self.shared_credentials = Config(do_load=False)
  177. if os.path.isfile(shared_path):
  178. self.shared_credentials.load_from_path(shared_path)
  179. self.get_credentials(access_key, secret_key, security_token, profile_name)
  180. self.configure_headers()
  181. self.configure_errors()
  182. # Allow config file to override default host and port.
  183. host_opt_name = '%s_host' % self.HostKeyMap[self.name]
  184. if config.has_option('Credentials', host_opt_name):
  185. self.host = config.get('Credentials', host_opt_name)
  186. port_opt_name = '%s_port' % self.HostKeyMap[self.name]
  187. if config.has_option('Credentials', port_opt_name):
  188. self.port = config.getint('Credentials', port_opt_name)
  189. host_header_opt_name = '%s_host_header' % self.HostKeyMap[self.name]
  190. if config.has_option('Credentials', host_header_opt_name):
  191. self.host_header = config.get('Credentials', host_header_opt_name)
  192. def get_access_key(self):
  193. if self._credentials_need_refresh():
  194. self._populate_keys_from_metadata_server()
  195. return self._access_key
  196. def set_access_key(self, value):
  197. self._access_key = value
  198. access_key = property(get_access_key, set_access_key)
  199. def get_secret_key(self):
  200. if self._credentials_need_refresh():
  201. self._populate_keys_from_metadata_server()
  202. return self._secret_key
  203. def set_secret_key(self, value):
  204. self._secret_key = value
  205. secret_key = property(get_secret_key, set_secret_key)
  206. def get_security_token(self):
  207. if self._credentials_need_refresh():
  208. self._populate_keys_from_metadata_server()
  209. return self._security_token
  210. def set_security_token(self, value):
  211. self._security_token = value
  212. security_token = property(get_security_token, set_security_token)
  213. def _credentials_need_refresh(self):
  214. if self._credential_expiry_time is None:
  215. return False
  216. else:
  217. # The credentials should be refreshed if they're going to expire
  218. # in less than 5 minutes.
  219. delta = self._credential_expiry_time - datetime.utcnow()
  220. # python2.6 does not have timedelta.total_seconds() so we have
  221. # to calculate this ourselves. This is straight from the
  222. # datetime docs.
  223. seconds_left = (
  224. (delta.microseconds + (delta.seconds + delta.days * 24 * 3600)
  225. * 10 ** 6) / 10 ** 6)
  226. if seconds_left < (5 * 60):
  227. boto.log.debug("Credentials need to be refreshed.")
  228. return True
  229. else:
  230. return False
  231. def get_credentials(self, access_key=None, secret_key=None,
  232. security_token=None, profile_name=None):
  233. access_key_name, secret_key_name, security_token_name, \
  234. profile_name_name = self.CredentialMap[self.name]
  235. # Load profile from shared environment variable if it was not
  236. # already passed in and the environment variable exists
  237. if profile_name is None and profile_name_name is not None and \
  238. profile_name_name.upper() in os.environ:
  239. profile_name = os.environ[profile_name_name.upper()]
  240. shared = self.shared_credentials
  241. if access_key is not None:
  242. self.access_key = access_key
  243. boto.log.debug("Using access key provided by client.")
  244. elif access_key_name.upper() in os.environ:
  245. self.access_key = os.environ[access_key_name.upper()]
  246. boto.log.debug("Using access key found in environment variable.")
  247. elif profile_name is not None:
  248. if shared.has_option(profile_name, access_key_name):
  249. self.access_key = shared.get(profile_name, access_key_name)
  250. boto.log.debug("Using access key found in shared credential "
  251. "file for profile %s." % profile_name)
  252. elif config.has_option("profile %s" % profile_name,
  253. access_key_name):
  254. self.access_key = config.get("profile %s" % profile_name,
  255. access_key_name)
  256. boto.log.debug("Using access key found in config file: "
  257. "profile %s." % profile_name)
  258. else:
  259. raise ProfileNotFoundError('Profile "%s" not found!' %
  260. profile_name)
  261. elif shared.has_option('default', access_key_name):
  262. self.access_key = shared.get('default', access_key_name)
  263. boto.log.debug("Using access key found in shared credential file.")
  264. elif config.has_option('Credentials', access_key_name):
  265. self.access_key = config.get('Credentials', access_key_name)
  266. boto.log.debug("Using access key found in config file.")
  267. if secret_key is not None:
  268. self.secret_key = secret_key
  269. boto.log.debug("Using secret key provided by client.")
  270. elif secret_key_name.upper() in os.environ:
  271. self.secret_key = os.environ[secret_key_name.upper()]
  272. boto.log.debug("Using secret key found in environment variable.")
  273. elif profile_name is not None:
  274. if shared.has_option(profile_name, secret_key_name):
  275. self.secret_key = shared.get(profile_name, secret_key_name)
  276. boto.log.debug("Using secret key found in shared credential "
  277. "file for profile %s." % profile_name)
  278. elif config.has_option("profile %s" % profile_name, secret_key_name):
  279. self.secret_key = config.get("profile %s" % profile_name,
  280. secret_key_name)
  281. boto.log.debug("Using secret key found in config file: "
  282. "profile %s." % profile_name)
  283. else:
  284. raise ProfileNotFoundError('Profile "%s" not found!' %
  285. profile_name)
  286. elif shared.has_option('default', secret_key_name):
  287. self.secret_key = shared.get('default', secret_key_name)
  288. boto.log.debug("Using secret key found in shared credential file.")
  289. elif config.has_option('Credentials', secret_key_name):
  290. self.secret_key = config.get('Credentials', secret_key_name)
  291. boto.log.debug("Using secret key found in config file.")
  292. elif config.has_option('Credentials', 'keyring'):
  293. keyring_name = config.get('Credentials', 'keyring')
  294. try:
  295. import keyring
  296. except ImportError:
  297. boto.log.error("The keyring module could not be imported. "
  298. "For keyring support, install the keyring "
  299. "module.")
  300. raise
  301. self.secret_key = keyring.get_password(
  302. keyring_name, self.access_key)
  303. boto.log.debug("Using secret key found in keyring.")
  304. if security_token is not None:
  305. self.security_token = security_token
  306. boto.log.debug("Using security token provided by client.")
  307. elif ((security_token_name is not None) and
  308. (access_key is None) and (secret_key is None)):
  309. # Only provide a token from the environment/config if the
  310. # caller did not specify a key and secret. Otherwise an
  311. # environment/config token could be paired with a
  312. # different set of credentials provided by the caller
  313. if security_token_name.upper() in os.environ:
  314. self.security_token = os.environ[security_token_name.upper()]
  315. boto.log.debug("Using security token found in environment"
  316. " variable.")
  317. elif shared.has_option(profile_name or 'default',
  318. security_token_name):
  319. self.security_token = shared.get(profile_name or 'default',
  320. security_token_name)
  321. boto.log.debug("Using security token found in shared "
  322. "credential file.")
  323. elif profile_name is not None:
  324. if config.has_option("profile %s" % profile_name,
  325. security_token_name):
  326. boto.log.debug("config has option")
  327. self.security_token = config.get("profile %s" % profile_name,
  328. security_token_name)
  329. boto.log.debug("Using security token found in config file: "
  330. "profile %s." % profile_name)
  331. elif config.has_option('Credentials', security_token_name):
  332. self.security_token = config.get('Credentials',
  333. security_token_name)
  334. boto.log.debug("Using security token found in config file.")
  335. if ((self._access_key is None or self._secret_key is None) and
  336. self.MetadataServiceSupport[self.name]):
  337. self._populate_keys_from_metadata_server()
  338. self._secret_key = self._convert_key_to_str(self._secret_key)
  339. def _populate_keys_from_metadata_server(self):
  340. # get_instance_metadata is imported here because of a circular
  341. # dependency.
  342. boto.log.debug("Retrieving credentials from metadata server.")
  343. from boto.utils import get_instance_metadata
  344. timeout = config.getfloat('Boto', 'metadata_service_timeout', 1.0)
  345. attempts = config.getint('Boto', 'metadata_service_num_attempts', 1)
  346. # The num_retries arg is actually the total number of attempts made,
  347. # so the config options is named *_num_attempts to make this more
  348. # clear to users.
  349. metadata = get_instance_metadata(
  350. timeout=timeout, num_retries=attempts,
  351. data='meta-data/iam/security-credentials/')
  352. if metadata:
  353. creds = self._get_credentials_from_metadata(metadata)
  354. self._access_key = creds[0]
  355. self._secret_key = creds[1]
  356. self._security_token = creds[2]
  357. expires_at = creds[3]
  358. # I'm assuming there's only one role on the instance profile.
  359. self._credential_expiry_time = datetime.strptime(
  360. expires_at, "%Y-%m-%dT%H:%M:%SZ")
  361. boto.log.debug("Retrieved credentials will expire in %s at: %s",
  362. self._credential_expiry_time - datetime.now(),
  363. expires_at)
  364. def _get_credentials_from_metadata(self, metadata):
  365. # Given metadata, return a tuple of (access, secret, token, expiration)
  366. # On errors, an InvalidInstanceMetadataError will be raised.
  367. # The "metadata" is a lazy loaded dictionary means that it's possible
  368. # to still encounter errors as we traverse through the metadata dict.
  369. # We try to be careful and raise helpful error messages when this
  370. # happens.
  371. creds = list(metadata.values())[0]
  372. if not isinstance(creds, dict):
  373. # We want to special case a specific error condition which is
  374. # where get_instance_metadata() returns an empty string on
  375. # error conditions.
  376. if creds == '':
  377. msg = 'an empty string'
  378. else:
  379. msg = 'type: %s' % creds
  380. raise InvalidInstanceMetadataError("Expected a dict type of "
  381. "credentials instead received "
  382. "%s" % (msg))
  383. try:
  384. access_key = creds['AccessKeyId']
  385. secret_key = self._convert_key_to_str(creds['SecretAccessKey'])
  386. security_token = creds['Token']
  387. expires_at = creds['Expiration']
  388. except KeyError as e:
  389. raise InvalidInstanceMetadataError(
  390. "Credentials from instance metadata missing "
  391. "required key: %s" % e)
  392. return access_key, secret_key, security_token, expires_at
  393. def _convert_key_to_str(self, key):
  394. if isinstance(key, six.text_type):
  395. # the secret key must be bytes and not unicode to work
  396. # properly with hmac.new (see http://bugs.python.org/issue5285)
  397. return str(key)
  398. return key
  399. def configure_headers(self):
  400. header_info_map = self.HeaderInfoMap[self.name]
  401. self.metadata_prefix = header_info_map[METADATA_PREFIX_KEY]
  402. self.header_prefix = header_info_map[HEADER_PREFIX_KEY]
  403. self.acl_header = header_info_map[ACL_HEADER_KEY]
  404. self.auth_header = header_info_map[AUTH_HEADER_KEY]
  405. self.copy_source_header = header_info_map[COPY_SOURCE_HEADER_KEY]
  406. self.copy_source_version_id = header_info_map[
  407. COPY_SOURCE_VERSION_ID_HEADER_KEY]
  408. self.copy_source_range_header = header_info_map[
  409. COPY_SOURCE_RANGE_HEADER_KEY]
  410. self.date_header = header_info_map[DATE_HEADER_KEY]
  411. self.delete_marker = header_info_map[DELETE_MARKER_HEADER_KEY]
  412. self.metadata_directive_header = (
  413. header_info_map[METADATA_DIRECTIVE_HEADER_KEY])
  414. self.security_token_header = header_info_map[SECURITY_TOKEN_HEADER_KEY]
  415. self.resumable_upload_header = (
  416. header_info_map[RESUMABLE_UPLOAD_HEADER_KEY])
  417. self.server_side_encryption_header = header_info_map[SERVER_SIDE_ENCRYPTION_KEY]
  418. self.storage_class_header = header_info_map[STORAGE_CLASS_HEADER_KEY]
  419. self.version_id = header_info_map[VERSION_ID_HEADER_KEY]
  420. self.mfa_header = header_info_map[MFA_HEADER_KEY]
  421. self.restore_header = header_info_map[RESTORE_HEADER_KEY]
  422. def configure_errors(self):
  423. error_map = self.ErrorMap[self.name]
  424. self.storage_copy_error = error_map[STORAGE_COPY_ERROR]
  425. self.storage_create_error = error_map[STORAGE_CREATE_ERROR]
  426. self.storage_data_error = error_map[STORAGE_DATA_ERROR]
  427. self.storage_permissions_error = error_map[STORAGE_PERMISSIONS_ERROR]
  428. self.storage_response_error = error_map[STORAGE_RESPONSE_ERROR]
  429. def get_provider_name(self):
  430. return self.HostKeyMap[self.name]
  431. def supports_chunked_transfer(self):
  432. return self.ChunkedTransferSupport[self.name]
  433. # Static utility method for getting default Provider.
  434. def get_default():
  435. return Provider('aws')