PageRenderTime 53ms CodeModel.GetById 13ms app.highlight 35ms RepoModel.GetById 1ms app.codeStats 0ms

/boto-2.5.2/boto/auth.py

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