PageRenderTime 36ms CodeModel.GetById 6ms RepoModel.GetById 0ms app.codeStats 0ms

/boto-2.5.2/boto/auth.py

#
Python | 456 lines | 356 code | 39 blank | 61 comment | 21 complexity | df159d64c469674c1346ffbb487a463a MD5 | raw file
  1. # Copyright 2010 Google Inc.
  2. # Copyright (c) 2011 Mitch Garnaat http://garnaat.org/
  3. # Copyright (c) 2011, Eucalyptus Systems, Inc.
  4. #
  5. # Permission is hereby granted, free of charge, to any person obtaining a
  6. # copy of this software and associated documentation files (the
  7. # "Software"), to deal in the Software without restriction, including
  8. # without limitation the rights to use, copy, modify, merge, publish, dis-
  9. # tribute, sublicense, and/or sell copies of the Software, and to permit
  10. # persons to whom the Software is furnished to do so, subject to the fol-
  11. # lowing conditions:
  12. #
  13. # The above copyright notice and this permission notice shall be included
  14. # in all copies or substantial portions of the Software.
  15. #
  16. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  17. # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
  18. # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
  19. # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  20. # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
  22. # IN THE SOFTWARE.
  23. """
  24. Handles authentication required to AWS and GS
  25. """
  26. import base64
  27. import boto
  28. import boto.auth_handler
  29. import boto.exception
  30. import boto.plugin
  31. import boto.utils
  32. import hmac
  33. import sys
  34. import urllib
  35. from email.utils import formatdate
  36. from boto.auth_handler import AuthHandler
  37. from boto.exception import BotoClientError
  38. #
  39. # the following is necessary because of the incompatibilities
  40. # between Python 2.4, 2.5, and 2.6 as well as the fact that some
  41. # people running 2.4 have installed hashlib as a separate module
  42. # this fix was provided by boto user mccormix.
  43. # see: http://code.google.com/p/boto/issues/detail?id=172
  44. # for more details.
  45. #
  46. try:
  47. from hashlib import sha1 as sha
  48. from hashlib import sha256 as sha256
  49. if sys.version[:3] == "2.4":
  50. # we are using an hmac that expects a .new() method.
  51. class Faker:
  52. def __init__(self, which):
  53. self.which = which
  54. self.digest_size = self.which().digest_size
  55. def new(self, *args, **kwargs):
  56. return self.which(*args, **kwargs)
  57. sha = Faker(sha)
  58. sha256 = Faker(sha256)
  59. except ImportError:
  60. import sha
  61. sha256 = None
  62. class HmacKeys(object):
  63. """Key based Auth handler helper."""
  64. def __init__(self, host, config, provider):
  65. if provider.access_key is None or provider.secret_key is None:
  66. raise boto.auth_handler.NotReadyToAuthenticate()
  67. self.host = host
  68. self.update_provider(provider)
  69. def update_provider(self, provider):
  70. self._provider = provider
  71. self._hmac = hmac.new(self._provider.secret_key, digestmod=sha)
  72. if sha256:
  73. self._hmac_256 = hmac.new(self._provider.secret_key,
  74. digestmod=sha256)
  75. else:
  76. self._hmac_256 = None
  77. def algorithm(self):
  78. if self._hmac_256:
  79. return 'HmacSHA256'
  80. else:
  81. return 'HmacSHA1'
  82. def sign_string(self, string_to_sign):
  83. if self._hmac_256:
  84. hmac = self._hmac_256.copy()
  85. else:
  86. hmac = self._hmac.copy()
  87. hmac.update(string_to_sign)
  88. return base64.encodestring(hmac.digest()).strip()
  89. class AnonAuthHandler(AuthHandler, HmacKeys):
  90. """
  91. Implements Anonymous requests.
  92. """
  93. capability = ['anon']
  94. def __init__(self, host, config, provider):
  95. AuthHandler.__init__(self, host, config, provider)
  96. def add_auth(self, http_request, **kwargs):
  97. pass
  98. class HmacAuthV1Handler(AuthHandler, HmacKeys):
  99. """ Implements the HMAC request signing used by S3 and GS."""
  100. capability = ['hmac-v1', 's3']
  101. def __init__(self, host, config, provider):
  102. AuthHandler.__init__(self, host, config, provider)
  103. HmacKeys.__init__(self, host, config, provider)
  104. self._hmac_256 = None
  105. def update_provider(self, provider):
  106. super(HmacAuthV1Handler, self).update_provider(provider)
  107. self._hmac_256 = None
  108. def add_auth(self, http_request, **kwargs):
  109. headers = http_request.headers
  110. method = http_request.method
  111. auth_path = http_request.auth_path
  112. if 'Date' not in headers:
  113. headers['Date'] = formatdate(usegmt=True)
  114. if self._provider.security_token:
  115. key = self._provider.security_token_header
  116. headers[key] = self._provider.security_token
  117. string_to_sign = boto.utils.canonical_string(method, auth_path,
  118. headers, None,
  119. self._provider)
  120. boto.log.debug('StringToSign:\n%s' % string_to_sign)
  121. b64_hmac = self.sign_string(string_to_sign)
  122. auth_hdr = self._provider.auth_header
  123. headers['Authorization'] = ("%s %s:%s" %
  124. (auth_hdr,
  125. self._provider.access_key, b64_hmac))
  126. class HmacAuthV2Handler(AuthHandler, HmacKeys):
  127. """
  128. Implements the simplified HMAC authorization used by CloudFront.
  129. """
  130. capability = ['hmac-v2', 'cloudfront']
  131. def __init__(self, host, config, provider):
  132. AuthHandler.__init__(self, host, config, provider)
  133. HmacKeys.__init__(self, host, config, provider)
  134. self._hmac_256 = None
  135. def update_provider(self, provider):
  136. super(HmacAuthV2Handler, self).update_provider(provider)
  137. self._hmac_256 = None
  138. def add_auth(self, http_request, **kwargs):
  139. headers = http_request.headers
  140. if 'Date' not in headers:
  141. headers['Date'] = formatdate(usegmt=True)
  142. b64_hmac = self.sign_string(headers['Date'])
  143. auth_hdr = self._provider.auth_header
  144. headers['Authorization'] = ("%s %s:%s" %
  145. (auth_hdr,
  146. self._provider.access_key, b64_hmac))
  147. class HmacAuthV3Handler(AuthHandler, HmacKeys):
  148. """Implements the new Version 3 HMAC authorization used by Route53."""
  149. capability = ['hmac-v3', 'route53', 'ses']
  150. def __init__(self, host, config, provider):
  151. AuthHandler.__init__(self, host, config, provider)
  152. HmacKeys.__init__(self, host, config, provider)
  153. def add_auth(self, http_request, **kwargs):
  154. headers = http_request.headers
  155. if 'Date' not in headers:
  156. headers['Date'] = formatdate(usegmt=True)
  157. b64_hmac = self.sign_string(headers['Date'])
  158. s = "AWS3-HTTPS AWSAccessKeyId=%s," % self._provider.access_key
  159. s += "Algorithm=%s,Signature=%s" % (self.algorithm(), b64_hmac)
  160. headers['X-Amzn-Authorization'] = s
  161. class HmacAuthV3HTTPHandler(AuthHandler, HmacKeys):
  162. """
  163. Implements the new Version 3 HMAC authorization used by DynamoDB.
  164. """
  165. capability = ['hmac-v3-http']
  166. def __init__(self, host, config, provider):
  167. AuthHandler.__init__(self, host, config, provider)
  168. HmacKeys.__init__(self, host, config, provider)
  169. def headers_to_sign(self, http_request):
  170. """
  171. Select the headers from the request that need to be included
  172. in the StringToSign.
  173. """
  174. headers_to_sign = {}
  175. headers_to_sign = {'Host' : self.host}
  176. for name, value in http_request.headers.items():
  177. lname = name.lower()
  178. if lname.startswith('x-amz'):
  179. headers_to_sign[name] = value
  180. return headers_to_sign
  181. def canonical_headers(self, headers_to_sign):
  182. """
  183. Return the headers that need to be included in the StringToSign
  184. in their canonical form by converting all header keys to lower
  185. case, sorting them in alphabetical order and then joining
  186. them into a string, separated by newlines.
  187. """
  188. l = sorted(['%s:%s'%(n.lower().strip(),
  189. headers_to_sign[n].strip()) for n in headers_to_sign])
  190. return '\n'.join(l)
  191. def string_to_sign(self, http_request):
  192. """
  193. Return the canonical StringToSign as well as a dict
  194. containing the original version of all headers that
  195. were included in the StringToSign.
  196. """
  197. headers_to_sign = self.headers_to_sign(http_request)
  198. canonical_headers = self.canonical_headers(headers_to_sign)
  199. string_to_sign = '\n'.join([http_request.method,
  200. http_request.path,
  201. '',
  202. canonical_headers,
  203. '',
  204. http_request.body])
  205. return string_to_sign, headers_to_sign
  206. def add_auth(self, req, **kwargs):
  207. """
  208. Add AWS3 authentication to a request.
  209. :type req: :class`boto.connection.HTTPRequest`
  210. :param req: The HTTPRequest object.
  211. """
  212. # This could be a retry. Make sure the previous
  213. # authorization header is removed first.
  214. if 'X-Amzn-Authorization' in req.headers:
  215. del req.headers['X-Amzn-Authorization']
  216. req.headers['X-Amz-Date'] = formatdate(usegmt=True)
  217. if self._provider.security_token:
  218. req.headers['X-Amz-Security-Token'] = self._provider.security_token
  219. string_to_sign, headers_to_sign = self.string_to_sign(req)
  220. boto.log.debug('StringToSign:\n%s' % string_to_sign)
  221. hash_value = sha256(string_to_sign).digest()
  222. b64_hmac = self.sign_string(hash_value)
  223. s = "AWS3 AWSAccessKeyId=%s," % self._provider.access_key
  224. s += "Algorithm=%s," % self.algorithm()
  225. s += "SignedHeaders=%s," % ';'.join(headers_to_sign)
  226. s += "Signature=%s" % b64_hmac
  227. req.headers['X-Amzn-Authorization'] = s
  228. class QuerySignatureHelper(HmacKeys):
  229. """
  230. Helper for Query signature based Auth handler.
  231. Concrete sub class need to implement _calc_sigature method.
  232. """
  233. def add_auth(self, http_request, **kwargs):
  234. headers = http_request.headers
  235. params = http_request.params
  236. params['AWSAccessKeyId'] = self._provider.access_key
  237. params['SignatureVersion'] = self.SignatureVersion
  238. params['Timestamp'] = boto.utils.get_ts()
  239. qs, signature = self._calc_signature(
  240. http_request.params, http_request.method,
  241. http_request.auth_path, http_request.host)
  242. boto.log.debug('query_string: %s Signature: %s' % (qs, signature))
  243. if http_request.method == 'POST':
  244. headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
  245. http_request.body = qs + '&Signature=' + urllib.quote_plus(signature)
  246. http_request.headers['Content-Length'] = str(len(http_request.body))
  247. else:
  248. http_request.body = ''
  249. # if this is a retried request, the qs from the previous try will
  250. # already be there, we need to get rid of that and rebuild it
  251. http_request.path = http_request.path.split('?')[0]
  252. http_request.path = (http_request.path + '?' + qs +
  253. '&Signature=' + urllib.quote_plus(signature))
  254. class QuerySignatureV0AuthHandler(QuerySignatureHelper, AuthHandler):
  255. """Provides Signature V0 Signing"""
  256. SignatureVersion = 0
  257. capability = ['sign-v0']
  258. def _calc_signature(self, params, *args):
  259. boto.log.debug('using _calc_signature_0')
  260. hmac = self._hmac.copy()
  261. s = params['Action'] + params['Timestamp']
  262. hmac.update(s)
  263. keys = params.keys()
  264. keys.sort(cmp = lambda x, y: cmp(x.lower(), y.lower()))
  265. pairs = []
  266. for key in keys:
  267. val = boto.utils.get_utf8_value(params[key])
  268. pairs.append(key + '=' + urllib.quote(val))
  269. qs = '&'.join(pairs)
  270. return (qs, base64.b64encode(hmac.digest()))
  271. class QuerySignatureV1AuthHandler(QuerySignatureHelper, AuthHandler):
  272. """
  273. Provides Query Signature V1 Authentication.
  274. """
  275. SignatureVersion = 1
  276. capability = ['sign-v1', 'mturk']
  277. def _calc_signature(self, params, *args):
  278. boto.log.debug('using _calc_signature_1')
  279. hmac = self._hmac.copy()
  280. keys = params.keys()
  281. keys.sort(cmp = lambda x, y: cmp(x.lower(), y.lower()))
  282. pairs = []
  283. for key in keys:
  284. hmac.update(key)
  285. val = boto.utils.get_utf8_value(params[key])
  286. hmac.update(val)
  287. pairs.append(key + '=' + urllib.quote(val))
  288. qs = '&'.join(pairs)
  289. return (qs, base64.b64encode(hmac.digest()))
  290. class QuerySignatureV2AuthHandler(QuerySignatureHelper, AuthHandler):
  291. """Provides Query Signature V2 Authentication."""
  292. SignatureVersion = 2
  293. capability = ['sign-v2', 'ec2', 'ec2', 'emr', 'fps', 'ecs',
  294. 'sdb', 'iam', 'rds', 'sns', 'sqs', 'cloudformation']
  295. def _calc_signature(self, params, verb, path, server_name):
  296. boto.log.debug('using _calc_signature_2')
  297. string_to_sign = '%s\n%s\n%s\n' % (verb, server_name.lower(), path)
  298. if self._hmac_256:
  299. hmac = self._hmac_256.copy()
  300. params['SignatureMethod'] = 'HmacSHA256'
  301. else:
  302. hmac = self._hmac.copy()
  303. params['SignatureMethod'] = 'HmacSHA1'
  304. if self._provider.security_token:
  305. params['SecurityToken'] = self._provider.security_token
  306. keys = sorted(params.keys())
  307. pairs = []
  308. for key in keys:
  309. val = boto.utils.get_utf8_value(params[key])
  310. pairs.append(urllib.quote(key, safe='') + '=' +
  311. urllib.quote(val, safe='-_~'))
  312. qs = '&'.join(pairs)
  313. boto.log.debug('query string: %s' % qs)
  314. string_to_sign += qs
  315. boto.log.debug('string_to_sign: %s' % string_to_sign)
  316. hmac.update(string_to_sign)
  317. b64 = base64.b64encode(hmac.digest())
  318. boto.log.debug('len(b64)=%d' % len(b64))
  319. boto.log.debug('base64 encoded digest: %s' % b64)
  320. return (qs, b64)
  321. class POSTPathQSV2AuthHandler(QuerySignatureV2AuthHandler, AuthHandler):
  322. """
  323. Query Signature V2 Authentication relocating signed query
  324. into the path and allowing POST requests with Content-Types.
  325. """
  326. capability = ['mws']
  327. def add_auth(self, req, **kwargs):
  328. req.params['AWSAccessKeyId'] = self._provider.access_key
  329. req.params['SignatureVersion'] = self.SignatureVersion
  330. req.params['Timestamp'] = boto.utils.get_ts()
  331. qs, signature = self._calc_signature(req.params, req.method,
  332. req.auth_path, req.host)
  333. boto.log.debug('query_string: %s Signature: %s' % (qs, signature))
  334. if req.method == 'POST':
  335. req.headers['Content-Length'] = str(len(req.body))
  336. req.headers['Content-Type'] = req.headers.get('Content-Type',
  337. 'text/plain')
  338. else:
  339. req.body = ''
  340. # if this is a retried req, the qs from the previous try will
  341. # already be there, we need to get rid of that and rebuild it
  342. req.path = req.path.split('?')[0]
  343. req.path = (req.path + '?' + qs +
  344. '&Signature=' + urllib.quote_plus(signature))
  345. def get_auth_handler(host, config, provider, requested_capability=None):
  346. """Finds an AuthHandler that is ready to authenticate.
  347. Lists through all the registered AuthHandlers to find one that is willing
  348. to handle for the requested capabilities, config and provider.
  349. :type host: string
  350. :param host: The name of the host
  351. :type config:
  352. :param config:
  353. :type provider:
  354. :param provider:
  355. Returns:
  356. An implementation of AuthHandler.
  357. Raises:
  358. boto.exception.NoAuthHandlerFound:
  359. boto.exception.TooManyAuthHandlerReadyToAuthenticate:
  360. """
  361. ready_handlers = []
  362. auth_handlers = boto.plugin.get_plugin(AuthHandler, requested_capability)
  363. total_handlers = len(auth_handlers)
  364. for handler in auth_handlers:
  365. try:
  366. ready_handlers.append(handler(host, config, provider))
  367. except boto.auth_handler.NotReadyToAuthenticate:
  368. pass
  369. if not ready_handlers:
  370. checked_handlers = auth_handlers
  371. names = [handler.__name__ for handler in checked_handlers]
  372. raise boto.exception.NoAuthHandlerFound(
  373. 'No handler was ready to authenticate. %d handlers were checked.'
  374. ' %s '
  375. 'Check your credentials' % (len(names), str(names)))
  376. if len(ready_handlers) > 1:
  377. # NOTE: Even though it would be nice to accept more than one handler
  378. # by using one of the many ready handlers, we are never sure that each
  379. # of them are referring to the same storage account. Since we cannot
  380. # easily guarantee that, it is always safe to fail, rather than operate
  381. # on the wrong account.
  382. names = [handler.__class__.__name__ for handler in ready_handlers]
  383. raise boto.exception.TooManyAuthHandlerReadyToAuthenticate(
  384. '%d AuthHandlers %s ready to authenticate for requested_capability '
  385. '%s, only 1 expected. This happens if you import multiple '
  386. 'pluging.Plugin implementations that declare support for the '
  387. 'requested_capability.' % (len(names), str(names),
  388. requested_capability))
  389. return ready_handlers[0]