[Python-modules-commits] [python-aws-requests-auth] 01/02: New upstream version 0.4.1

Sophie Brun sbrun-guest at moszumanska.debian.org
Wed Oct 25 09:53:57 UTC 2017


This is an automated email from the git hooks/post-receive script.

sbrun-guest pushed a commit to branch debian/master
in repository python-aws-requests-auth.

commit 950fcf677d654e319717d9ef8ad95553eb885a14
Author: Sophie Brun <sophie at freexian.com>
Date:   Wed Oct 25 11:39:00 2017 +0200

    New upstream version 0.4.1
---
 CHANGELOG.md                               |  76 +++++++++
 LICENSE                                    |  27 ++++
 MANIFEST.in                                |   7 +
 PKG-INFO                                   |  13 ++
 README.md                                  | 113 ++++++++++++++
 aws_requests_auth/__init__.py              |   0
 aws_requests_auth/aws_auth.py              | 239 +++++++++++++++++++++++++++++
 aws_requests_auth/boto_utils.py            |  53 +++++++
 aws_requests_auth/tests/__init__.py        |   0
 aws_requests_auth/tests/test_aws_auth.py   | 216 ++++++++++++++++++++++++++
 aws_requests_auth/tests/test_boto_utils.py |  59 +++++++
 setup.py                                   |  19 +++
 12 files changed, 822 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..63b8a86
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,76 @@
+Changelog (aws-requests-auth)
+==================
+
+0.4.1
+------------------
+- Allow utf-8 encoding failures for python2 on the request body for hashing
+    - Contributed by @bigjust: https://github.com/DavidMuller/aws-requests-auth/pull/30
+
+
+0.4.0
+------------------
+- Add `BotoAWSRequestsAuth` convenience class which automatically gathers (and refreshes) AWS credentials using botocore
+    - Contributed by @tobiasmcnulty: https://github.com/DavidMuller/aws-requests-auth/pull/29
+
+
+0.3.3
+------------------
+- Add classifiers to the pypi distribution
+
+
+0.3.2
+------------------
+- Add convenience methods for dynamically pulling AWS credentials via boto3
+    - Thanks to @schobster: https://github.com/DavidMuller/aws-requests-auth/pull/22
+
+
+0.3.1
+------------------
+- Patch encoding error on python 3.6
+    - See https://github.com/DavidMuller/aws-requests-auth/pull/21
+
+
+0.3.0
+------------------
+- Add python3 support -- thanks to @jlaine, and @anantasty
+   - See https://github.com/DavidMuller/aws-requests-auth/pull/16
+
+0.2.5
+------------------
+- Stop urlencoding query params in get_canonical_querystring(). The urlencoding in get_canonical_querystring() was causing "double encoding issues" because elasticsearch-py already apperas to urlencode query params
+    - If you are using a client other than elasticsearch-py, you will need to be sure that your client urlecondes your query params before they are passed to the `AWSRequests` auth class
+    - See https://github.com/DavidMuller/aws-requests-auth/pull/13 for more details
+
+0.2.4
+------------------
+- Add support for [AWS STS](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html) using the `aws_token` keyword argument to `AWSRequestsAuth`
+    - See [issue #9](https://github.com/DavidMuller/aws-requests-auth/issues/9) and [PR #11](https://github.com/DavidMuller/aws-requests-auth/pull/11 for) for additional details
+
+0.2.3
+------------------
+- Fix handling of multiple query parameters
+    - For example, the two `pretty=True` query paramaters in the following url
+      `http://search-service-foobar.us-east-1.es.amazonaws.com?pretty=True&pretty=True`
+      are now handled properly
+    - see https://github.com/DavidMuller/aws-requests-auth/pull/7
+
+
+0.2.2
+------------------
+- Update url quoting for canonical uri and canonical query string
+
+
+0.2.1
+------------------
+- Fix bug where cannonical uri and query string was not url encoded appropriately for the signing process
+
+
+0.2.0
+------------------
+- Fix typos of `aws_secret_access_key` : https://github.com/DavidMuller/aws-requests-auth/pull/1
+    - This is a breaking change. The `AWSRequestsAuth` constructor now expects the kwarg `aws_secret_access_key` (instead of the incorrectly spelled `aws_secret_acces_key`).
+
+
+0.1.0
+------------------
+Initial release
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a94d8cc
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) David Muller.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    1. Redistributions of source code must retain the above copyright notice,
+       this list of conditions and the following disclaimer.
+
+    2. Redistributions in binary form must reproduce the above copyright
+       notice, this list of conditions and the following disclaimer in the
+       documentation and/or other materials provided with the distribution.
+
+    3. The names of its contributors may not be used to endorse or promote
+       products derived from this software without specific prior written
+       permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..8c02389
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,7 @@
+include README.md
+include LICENSE
+include CHANGELOG.md
+include MANIFEST.in
+recursive-include aws_requests_auth *
+recursive-exclude * __pycache__
+recursive-exclude * *.py[co]
\ No newline at end of file
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..e56852e
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,13 @@
+Metadata-Version: 1.1
+Name: aws-requests-auth
+Version: 0.4.1
+Summary: AWS signature version 4 signing process for the python requests module
+Home-page: https://github.com/davidmuller/aws-requests-auth
+Author: David Muller
+Author-email: davehmuller at gmail.com
+License: UNKNOWN
+Description: See https://github.com/davidmuller/aws-requests-auth for installation and usage instructions.
+Platform: UNKNOWN
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e2e6889
--- /dev/null
+++ b/README.md
@@ -0,0 +1,113 @@
+[![Build Status](https://travis-ci.org/DavidMuller/aws-requests-auth.svg?branch=master)](https://travis-ci.org/DavidMuller/aws-requests-auth)
+
+# AWS Signature Version 4 Signing Process with python requests
+
+This package allows you to authenticate to AWS with Amazon's [signature version 4 signing process](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html) with the python [requests](http://docs.python-requests.org/en/latest/) library.
+
+Tested with both python `2.7` and `3.4`.
+
+# Installation
+
+```
+pip install aws-requests-auth
+```
+
+# Motivation
+
+This code came about because Amazon's Elasticsearch Service [does not currently support VPC](https://forums.aws.amazon.com/thread.jspa?threadID=217059&tstart=0). This authentication class allows us to talk to our Elasticsearch cluster via [IAM](https://aws.amazon.com/iam/).
+
+Conceivably, the authentication class is flexible enough to be used with any AWS service that supports the signature version 4 signing process.  However, I've only tested it with the Elasticsearch service.
+
+
+# Usage
+
+```python
+import requests
+from aws_requests_auth.aws_auth import AWSRequestsAuth
+
+# let's talk to our AWS Elasticsearch cluster
+auth = AWSRequestsAuth(aws_access_key='YOURKEY',
+                       aws_secret_access_key='YOURSECRET',
+                       aws_host='search-service-foobar.us-east-1.es.amazonaws.com',
+                       aws_region='us-east-1',
+                       aws_service='es')
+
+response = requests.get('http://search-service-foobar.us-east-1.es.amazonaws.com',
+                        auth=auth)
+print response.content
+
+{
+  "status" : 200,
+  "name" : "Stevie Hunter",
+  "cluster_name" : "elasticsearch",
+  "version" : {
+    "number" : "1.5.2",
+    etc....
+  },
+  "tagline" : "You Know, for Search"
+}
+```
+
+## elasticsearch-py Client Usage Example
+
+It's possible to inject the `AWSRequestsAuth` class directly into the [elasticsearch-py](https://elasticsearch-py.readthedocs.org/en/master/) library so you can talk to your Amazon AWS cluster directly through the elasticsearch-py client.
+
+```python
+from aws_requests_auth.aws_auth import AWSRequestsAuth
+from elasticsearch import Elasticsearch, RequestsHttpConnection
+
+es_host = 'search-service-foobar.us-east-1.es.amazonaws.com'
+auth = AWSRequestsAuth(aws_access_key='YOURKEY',
+                       aws_secret_access_key='YOURSECRET',
+                       aws_host=es_host,
+                       aws_region='us-east-1',
+                       aws_service='es')
+
+# use the requests connection_class and pass in our custom auth class
+es_client = Elasticsearch(host=es_host,
+                          port=80,
+                          connection_class=RequestsHttpConnection,
+                          http_auth=auth)
+print es_client.info()
+```
+
+## Temporary Security Credentials
+If you are using [AWS STS](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html) to grant temporary access to your Elasticsearch resource, you can use the `aws_token` keyword argument to include your credentials in `AWSRequestsAuth`.  See [issue #9](https://github.com/DavidMuller/aws-requests-auth/issues/9) and [PR #11](https://github.com/DavidMuller/aws-requests-auth/pull/11) for additional details.
+
+## AWS Lambda Quickstart Example
+If you are using an AWS lamba to talk to your Elasticsearch cluster and you've assigned an IAM role to your lambda function that allows the lambda to communicate with your Elasticserach cluster, you can instantiate an instance of AWSRequestsAuth by reading your credentials from environment variables:
+```python
+import os
+from aws_requests_auth.aws_auth import AWSRequestsAuth
+
+def lambda_handler(event, context):
+    auth = AWSRequestsAuth(aws_access_key=os.environ['AWS_ACCESS_KEY_ID'],
+                           aws_secret_access_key=os.environ['AWS_SECRET_ACCESS_KEY'],
+                           aws_token=os.environ['AWS_SESSION_TOKEN'],
+                           aws_host='search-service-foobar.us-east-1.es.amazonaws.com',
+                           aws_region='us-east-1',
+                           aws_service='es')
+    print 'My lambda finished executing'                           
+```
+`'AWS_ACCESS_KEY_ID'`, `'AWS_SECRET_ACCESS_KEY'`, `'AWS_SESSION_TOKEN'` are [reserved environment variables in AWS lambdas](https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html#lambda-environment-variables).
+
+## Using Boto To Automatically Gather AWS Credentials
+`botocore` (the core functionality of `boto3`) is not a strict requirement of `aws-requests-auth`, but we do provide some convenience methods if you'd like to use `botocore` to automatically retrieve your AWS credentials for you.
+
+`botocore` [can dynamically pull AWS credentials from environment variables, AWS config files, IAM Role,
+and other locations](http://boto3.readthedocs.io/en/latest/guide/configuration.html#configuring-credentials). Dynamic credential fetching can come in handy if you need to run a program leveraging `aws-requests-auth` in several places where you may authenticate in different manners. For example, you may rely on a `.aws/credentials` file when running on your local machine, but use an IAM role when running your program in a docker container in the cloud.
+
+To take advantage of these conveniences, and help you authenticate wherever `botocore` finds AWS credentials, you can import the `boto_utils` file and initialize `BotoAWSRequestsAuth` as follows:
+
+```python
+# note that this line will fail if you do not have botocore installed
+# botocore installation instructions available here:
+# https://boto3.readthedocs.io/en/latest/guide/quickstart.html#installation
+from aws_requests_auth.boto_utils import BotoAWSRequestsAuth
+
+auth = BotoAWSRequestsAuth(aws_host='search-service-foobar.us-east-1.es.amazonaws.com',
+                           aws_region='us-east-1',
+                           aws_service='es')
+```
+
+Credentials are only accessed when needed at runtime, and they will be refreshed using the underlying methods in `botocore` if needed.
diff --git a/aws_requests_auth/__init__.py b/aws_requests_auth/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/aws_requests_auth/aws_auth.py b/aws_requests_auth/aws_auth.py
new file mode 100644
index 0000000..11c57c4
--- /dev/null
+++ b/aws_requests_auth/aws_auth.py
@@ -0,0 +1,239 @@
+import hmac
+import hashlib
+import datetime
+
+try:
+    # python 2
+    from urllib import quote
+    from urlparse import urlparse
+except ImportError:
+    # python 3
+    from urllib.parse import quote, urlparse
+
+import requests
+
+
+def sign(key, msg):
+    """
+    Copied from https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html
+    """
+    return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()
+
+
+def getSignatureKey(key, dateStamp, regionName, serviceName):
+    """
+    Copied from https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html
+    """
+    kDate = sign(('AWS4' + key).encode('utf-8'), dateStamp)
+    kRegion = sign(kDate, regionName)
+    kService = sign(kRegion, serviceName)
+    kSigning = sign(kService, 'aws4_request')
+    return kSigning
+
+
+class AWSRequestsAuth(requests.auth.AuthBase):
+    """
+    Auth class that allows us to connect to AWS services
+    via Amazon's signature version 4 signing process
+
+    Adapted from https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html
+    """
+
+    def __init__(self,
+                 aws_access_key,
+                 aws_secret_access_key,
+                 aws_host,
+                 aws_region,
+                 aws_service,
+                 aws_token=None):
+        """
+        Example usage for talking to an AWS Elasticsearch Service:
+
+        AWSRequestsAuth(aws_access_key='YOURKEY',
+                        aws_secret_access_key='YOURSECRET',
+                        aws_host='search-service-foobar.us-east-1.es.amazonaws.com',
+                        aws_region='us-east-1',
+                        aws_service='es',
+                        aws_token='...')
+
+        The aws_token is optional and is used only if you are using STS
+        temporary credentials.
+        """
+        self.aws_access_key = aws_access_key
+        self.aws_secret_access_key = aws_secret_access_key
+        self.aws_host = aws_host
+        self.aws_region = aws_region
+        self.service = aws_service
+        self.aws_token = aws_token
+
+    def __call__(self, r):
+        """
+        Adds the authorization headers required by Amazon's signature
+        version 4 signing process to the request.
+
+        Adapted from https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html
+        """
+        aws_headers = self.get_aws_request_headers_handler(r)
+        r.headers.update(aws_headers)
+        return r
+
+    def get_aws_request_headers_handler(self, r):
+        """
+        Override get_aws_request_headers_handler() if you have a
+        subclass that needs to call get_aws_request_headers() with
+        an arbitrary set of AWS credentials. The default implementation
+        calls get_aws_request_headers() with self.aws_access_key,
+        self.aws_secret_access_key, and self.aws_token
+        """
+        return self.get_aws_request_headers(r=r,
+                                            aws_access_key=self.aws_access_key,
+                                            aws_secret_access_key=self.aws_secret_access_key,
+                                            aws_token=self.aws_token)
+
+    def get_aws_request_headers(self, r, aws_access_key, aws_secret_access_key, aws_token):
+        """
+        Returns a dictionary containing the necessary headers for Amazon's
+        signature version 4 signing process. An example return value might
+        look like
+
+            {
+                'Authorization': 'AWS4-HMAC-SHA256 Credential=YOURKEY/20160618/us-east-1/es/aws4_request, '
+                                 'SignedHeaders=host;x-amz-date, '
+                                 'Signature=ca0a856286efce2a4bd96a978ca6c8966057e53184776c0685169d08abd74739',
+                'x-amz-date': '20160618T220405Z',
+            }
+        """
+        # Create a date for headers and the credential string
+        t = datetime.datetime.utcnow()
+        amzdate = t.strftime('%Y%m%dT%H%M%SZ')
+        datestamp = t.strftime('%Y%m%d')  # Date w/o time for credential_scope
+
+        canonical_uri = AWSRequestsAuth.get_canonical_path(r)
+
+        canonical_querystring = AWSRequestsAuth.get_canonical_querystring(r)
+
+        # Create the canonical headers and signed headers. Header names
+        # and value must be trimmed and lowercase, and sorted in ASCII order.
+        # Note that there is a trailing \n.
+        canonical_headers = ('host:' + self.aws_host + '\n' +
+                             'x-amz-date:' + amzdate + '\n')
+        if aws_token:
+            canonical_headers += 'x-amz-security-token:' + aws_token + '\n'
+
+        # Create the list of signed headers. This lists the headers
+        # in the canonical_headers list, delimited with ";" and in alpha order.
+        # Note: The request can include any headers; canonical_headers and
+        # signed_headers lists those that you want to be included in the
+        # hash of the request. "Host" and "x-amz-date" are always required.
+        signed_headers = 'host;x-amz-date'
+        if aws_token:
+            signed_headers += ';x-amz-security-token'
+
+        # Create payload hash (hash of the request body content). For GET
+        # requests, the payload is an empty string ('').
+        body = r.body if r.body else bytes()
+        try:
+            body = body.encode('utf-8')
+        except (AttributeError, UnicodeDecodeError):
+            # On py2, if unicode characters in present in `body`,
+            # encode() throws UnicodeDecodeError, but we can safely
+            # pass unencoded `body` to execute hexdigest().
+            #
+            # For py3, encode() will execute successfully regardless
+            # of the presence of unicode data
+            body = body
+
+        payload_hash = hashlib.sha256(body).hexdigest()
+
+        # Combine elements to create create canonical request
+        canonical_request = (r.method + '\n' + canonical_uri + '\n' +
+                             canonical_querystring + '\n' + canonical_headers +
+                             '\n' + signed_headers + '\n' + payload_hash)
+
+        # Match the algorithm to the hashing algorithm you use, either SHA-1 or
+        # SHA-256 (recommended)
+        algorithm = 'AWS4-HMAC-SHA256'
+        credential_scope = (datestamp + '/' + self.aws_region + '/' +
+                            self.service + '/' + 'aws4_request')
+        string_to_sign = (algorithm + '\n' + amzdate + '\n' + credential_scope +
+                          '\n' + hashlib.sha256(canonical_request.encode('utf-8')).hexdigest())
+
+        # Create the signing key using the function defined above.
+        signing_key = getSignatureKey(aws_secret_access_key,
+                                      datestamp,
+                                      self.aws_region,
+                                      self.service)
+
+        # Sign the string_to_sign using the signing_key
+        string_to_sign_utf8 = string_to_sign.encode('utf-8')
+        signature = hmac.new(signing_key,
+                             string_to_sign_utf8,
+                             hashlib.sha256).hexdigest()
+
+        # The signing information can be either in a query string value or in
+        # a header named Authorization. This code shows how to use a header.
+        # Create authorization header and add to request headers
+        authorization_header = (algorithm + ' ' + 'Credential=' + aws_access_key +
+                                '/' + credential_scope + ', ' + 'SignedHeaders=' +
+                                signed_headers + ', ' + 'Signature=' + signature)
+
+        headers = {
+            'Authorization': authorization_header,
+            'x-amz-date': amzdate,
+        }
+        if aws_token:
+            headers['X-Amz-Security-Token'] = aws_token
+        return headers
+
+    @classmethod
+    def get_canonical_path(cls, r):
+        """
+        Create canonical URI--the part of the URI from domain to query
+        string (use '/' if no path)
+        """
+        parsedurl = urlparse(r.url)
+
+        # safe chars adapted from boto's use of urllib.parse.quote
+        # https://github.com/boto/boto/blob/d9e5cfe900e1a58717e393c76a6e3580305f217a/boto/auth.py#L393
+        return quote(parsedurl.path if parsedurl.path else '/', safe='/-_.~')
+
+    @classmethod
+    def get_canonical_querystring(cls, r):
+        """
+        Create the canonical query string. According to AWS, by the
+        end of this function our query string values must
+        be URL-encoded (space=%20) and the parameters must be sorted
+        by name.
+
+        This method assumes that the query params in `r` are *already*
+        url encoded.  If they are not url encoded by the time they make
+        it to this function, AWS may complain that the signature for your
+        request is incorrect.
+
+        It appears elasticsearc-py url encodes query paramaters on its own:
+            https://github.com/elastic/elasticsearch-py/blob/5dfd6985e5d32ea353d2b37d01c2521b2089ac2b/elasticsearch/connection/http_requests.py#L64
+
+        If you are using a different client than elasticsearch-py, it
+        will be your responsibility to urleconde your query params before
+        this method is called.
+        """
+        canonical_querystring = ''
+
+        parsedurl = urlparse(r.url)
+        querystring_sorted = '&'.join(sorted(parsedurl.query.split('&')))
+
+        for query_param in querystring_sorted.split('&'):
+            key_val_split = query_param.split('=', 1)
+
+            key = key_val_split[0]
+            if len(key_val_split) > 1:
+                val = key_val_split[1]
+            else:
+                val = ''
+
+            if key:
+                if canonical_querystring:
+                    canonical_querystring += "&"
+                canonical_querystring += u'='.join([key, val])
+
+        return canonical_querystring
diff --git a/aws_requests_auth/boto_utils.py b/aws_requests_auth/boto_utils.py
new file mode 100644
index 0000000..51fcc7a
--- /dev/null
+++ b/aws_requests_auth/boto_utils.py
@@ -0,0 +1,53 @@
+"""
+Functions in this file are included as a convenience for working with AWSRequestsAuth.
+External libraries, like boto, that this file imports are not a strict requirement for the
+aws-requests-auth package.
+"""
+
+from botocore.session import Session
+
+from .aws_auth import AWSRequestsAuth
+
+
+def get_credentials(credentials_obj=None):
+    """
+    Interacts with boto to retrieve AWS credentials, and returns a dictionary of
+    kwargs to be used in AWSRequestsAuth. boto automatically pulls AWS credentials from
+    a variety of sources including but not limited to credentials files and IAM role.
+    AWS credentials are pulled in the order listed here:
+    http://boto3.readthedocs.io/en/latest/guide/configuration.html#configuring-credentials
+    """
+    if credentials_obj is None:
+        credentials_obj = Session().get_credentials()
+    # use get_frozen_credentials to avoid the race condition where one or more
+    # properties may be refreshed and the other(s) not refreshed
+    frozen_credentials = credentials_obj.get_frozen_credentials()
+    return {
+        'aws_access_key': frozen_credentials.access_key,
+        'aws_secret_access_key': frozen_credentials.secret_key,
+        'aws_token': frozen_credentials.token,
+    }
+
+
+class BotoAWSRequestsAuth(AWSRequestsAuth):
+
+    def __init__(self, aws_host, aws_region, aws_service):
+        """
+        Example usage for talking to an AWS Elasticsearch Service:
+
+        BotoAWSRequestsAuth(aws_host='search-service-foobar.us-east-1.es.amazonaws.com',
+                            aws_region='us-east-1',
+                            aws_service='es')
+
+        The aws_access_key, aws_secret_access_key, and aws_token are discovered
+        automatically from the environment, in the order described here:
+        http://boto3.readthedocs.io/en/latest/guide/configuration.html#configuring-credentials
+        """
+        super(BotoAWSRequestsAuth, self).__init__(None, None, aws_host, aws_region, aws_service)
+        self._refreshable_credentials = Session().get_credentials()
+
+    def get_aws_request_headers_handler(self, r):
+        # provide credentials explicitly during each __call__, to take advantage
+        # of botocore's underlying logic to refresh expired credentials
+        credentials = get_credentials(self._refreshable_credentials)
+        return self.get_aws_request_headers(r, **credentials)
diff --git a/aws_requests_auth/tests/__init__.py b/aws_requests_auth/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/aws_requests_auth/tests/test_aws_auth.py b/aws_requests_auth/tests/test_aws_auth.py
new file mode 100644
index 0000000..feed53f
--- /dev/null
+++ b/aws_requests_auth/tests/test_aws_auth.py
@@ -0,0 +1,216 @@
+import datetime
+import mock
+import sys
+import unittest
+
+from aws_requests_auth.aws_auth import AWSRequestsAuth
+
+
+class TestAWSRequestsAuth(unittest.TestCase):
+    """
+    Tests for AWSRequestsAuth
+    """
+
+    def test_no_query_params(self):
+        """
+        Assert we generate the 'correct' cannonical query string
+        and canonical path for a request with no query params
+
+        Correct is relative here b/c 'correct' simply means what
+        the AWS Elasticsearch service expects
+        """
+        url = 'http://search-foo.us-east-1.es.amazonaws.com:80/'
+        mock_request = mock.Mock()
+        mock_request.url = url
+        self.assertEqual('/', AWSRequestsAuth.get_canonical_path(mock_request))
+        self.assertEqual('', AWSRequestsAuth.get_canonical_querystring(mock_request))
+
+    def test_characters_escaped_in_path(self):
+        """
+        Assert we generate the 'correct' cannonical query string
+        and path a request with characters that need to be escaped
+        """
+        url = 'http://search-foo.us-east-1.es.amazonaws.com:80/+foo.*/_stats'
+        mock_request = mock.Mock()
+        mock_request.url = url
+        self.assertEqual('/%2Bfoo.%2A/_stats', AWSRequestsAuth.get_canonical_path(mock_request))
+        self.assertEqual('', AWSRequestsAuth.get_canonical_querystring(mock_request))
+
+    def test_path_with_querystring(self):
+        """
+        Assert we generate the 'correct' cannonical query string
+        and path for request that includes a query stirng
+        """
+        url = 'http://search-foo.us-east-1.es.amazonaws.com:80/my_index/?pretty=True'
+        mock_request = mock.Mock()
+        mock_request.url = url
+        self.assertEqual('/my_index/', AWSRequestsAuth.get_canonical_path(mock_request))
+        self.assertEqual('pretty=True', AWSRequestsAuth.get_canonical_querystring(mock_request))
+
+    def test_multiple_get_params(self):
+        """
+        Assert we generate the 'correct' cannonical query string
+        for request that includes more than one query parameter
+        """
+        url = 'http://search-foo.us-east-1.es.amazonaws.com:80/index/type/_search?scroll=5m&search_type=scan'
+        mock_request = mock.Mock()
+        mock_request.url = url
+        self.assertEqual('scroll=5m&search_type=scan', AWSRequestsAuth.get_canonical_querystring(mock_request))
+
+    def test_post_request_with_get_param(self):
+        """
+        Assert we generate the 'correct' cannonical query string
+        for a post request that includes GET-parameters
+        """
+        url = 'http://search-foo.us-east-1.es.amazonaws.com:80/index/type/1/_update?version=1'
+        mock_request = mock.Mock()
+        mock_request.url = url
+        mock_request.method = "POST"
+        self.assertEqual('version=1', AWSRequestsAuth.get_canonical_querystring(mock_request))
+
+    def test_auth_for_get(self):
+        auth = AWSRequestsAuth(aws_access_key='YOURKEY',
+                               aws_secret_access_key='YOURSECRET',
+                               aws_host='search-foo.us-east-1.es.amazonaws.com',
+                               aws_region='us-east-1',
+                               aws_service='es')
+        url = 'http://search-foo.us-east-1.es.amazonaws.com:80/'
+        mock_request = mock.Mock()
+        mock_request.url = url
+        mock_request.method = "GET"
+        mock_request.body = None
+        mock_request.headers = {}
+
+        frozen_datetime = datetime.datetime(2016, 6, 18, 22, 4, 5)
+        with mock.patch('datetime.datetime') as mock_datetime:
+            mock_datetime.utcnow.return_value = frozen_datetime
+            auth(mock_request)
+        self.assertEqual({
+            'Authorization': 'AWS4-HMAC-SHA256 Credential=YOURKEY/20160618/us-east-1/es/aws4_request, '
+                             'SignedHeaders=host;x-amz-date, '
+                             'Signature=ca0a856286efce2a4bd96a978ca6c8966057e53184776c0685169d08abd74739',
+            'x-amz-date': '20160618T220405Z',
+
+        }, mock_request.headers)
+
+    def test_auth_for_post(self):
+        auth = AWSRequestsAuth(aws_access_key='YOURKEY',
+                               aws_secret_access_key='YOURSECRET',
+                               aws_host='search-foo.us-east-1.es.amazonaws.com',
+                               aws_region='us-east-1',
+                               aws_service='es')
+        url = 'http://search-foo.us-east-1.es.amazonaws.com:80/'
+        mock_request = mock.Mock()
+        mock_request.url = url
+        mock_request.method = "POST"
+        mock_request.body = b'foo=bar'
+        mock_request.headers = {
+            'Content-Type': 'application/x-www-form-urlencoded',
+        }
+
+        frozen_datetime = datetime.datetime(2016, 6, 18, 22, 4, 5)
+        with mock.patch('datetime.datetime') as mock_datetime:
+            mock_datetime.utcnow.return_value = frozen_datetime
+            auth(mock_request)
+        self.assertEqual({
+            'Authorization': 'AWS4-HMAC-SHA256 Credential=YOURKEY/20160618/us-east-1/es/aws4_request, '
+                             'SignedHeaders=host;x-amz-date, '
+                             'Signature=a6fd88e5f5c43e005482894001d9b05b43f6710e96be6098bcfcfccdeb8ed812',
+            'Content-Type': 'application/x-www-form-urlencoded',
+            'x-amz-date': '20160618T220405Z',
+
+        }, mock_request.headers)
+
+    def test_auth_for_post_with_str_body(self):
+        auth = AWSRequestsAuth(aws_access_key='YOURKEY',
+                               aws_secret_access_key='YOURSECRET',
+                               aws_host='search-foo.us-east-1.es.amazonaws.com',
+                               aws_region='us-east-1',
+                               aws_service='es')
+        url = 'http://search-foo.us-east-1.es.amazonaws.com:80/'
+        mock_request = mock.Mock()
+        mock_request.url = url
+        mock_request.method = "POST"
+        mock_request.body = 'foo=bar'
+        mock_request.headers = {
+            'Content-Type': 'application/x-www-form-urlencoded',
+        }
+
+        frozen_datetime = datetime.datetime(2016, 6, 18, 22, 4, 5)
+        with mock.patch('datetime.datetime') as mock_datetime:
+            mock_datetime.utcnow.return_value = frozen_datetime
+            auth(mock_request)
+        self.assertEqual({
+            'Authorization': 'AWS4-HMAC-SHA256 Credential=YOURKEY/20160618/us-east-1/es/aws4_request, '
+                             'SignedHeaders=host;x-amz-date, '
+                             'Signature=a6fd88e5f5c43e005482894001d9b05b43f6710e96be6098bcfcfccdeb8ed812',
+            'Content-Type': 'application/x-www-form-urlencoded',
+            'x-amz-date': '20160618T220405Z',
+
+        }, mock_request.headers)
+
+    @unittest.skipIf(
+        int(sys.version[0]) > 2,
+        'python3 produces a different hash that we\'re comparing.',
+    )
+    def test_auth_for_post_with_unicode_body_python2(self):
+        auth = AWSRequestsAuth(aws_access_key='YOURKEY',
+                               aws_secret_access_key='YOURSECRET',
+                               aws_host='search-foo.us-east-1.es.amazonaws.com',
+                               aws_region='us-east-1',
+                               aws_service='es')
+        url = 'http://search-foo.us-east-1.es.amazonaws.com:80/'
+        mock_request = mock.Mock()
+        mock_request.url = url
+        mock_request.method = "POST"
+        mock_request.body = 'foo=bar\xc3'
+        mock_request.headers = {
+            'Content-Type': 'application/x-www-form-urlencoded',
+        }
+
+        frozen_datetime = datetime.datetime(2016, 6, 18, 22, 4, 5)
+        with mock.patch('datetime.datetime') as mock_datetime:
+            mock_datetime.utcnow.return_value = frozen_datetime
+            auth(mock_request)
+
+        self.assertEqual({
+            'Authorization': 'AWS4-HMAC-SHA256 Credential=YOURKEY/20160618/us-east-1/es/aws4_request, '
+            'SignedHeaders=host;x-amz-date, '
+            'Signature=88046be72423b267de5e7e604aaffb2c5668c3fd9022ef4aac8287b82ab71124',
+            'Content-Type': 'application/x-www-form-urlencoded',
+            'x-amz-date': '20160618T220405Z',
+
+        }, mock_request.headers)
+
+    @unittest.skipIf(
+        int(sys.version[0]) < 3,
+        'python3 produces a different hash that we\'re comparing.'
+    )
+    def test_auth_for_post_with_unicode_body_python3(self):
+        auth = AWSRequestsAuth(aws_access_key='YOURKEY',
+                               aws_secret_access_key='YOURSECRET',
+                               aws_host='search-foo.us-east-1.es.amazonaws.com',
+                               aws_region='us-east-1',
+                               aws_service='es')
+        url = 'http://search-foo.us-east-1.es.amazonaws.com:80/'
+        mock_request = mock.Mock()
+        mock_request.url = url
+        mock_request.method = "POST"
+        mock_request.body = 'foo=bar\xc3'
+        mock_request.headers = {
+            'Content-Type': 'application/x-www-form-urlencoded',
+        }
+
+        frozen_datetime = datetime.datetime(2016, 6, 18, 22, 4, 5)
+        with mock.patch('datetime.datetime') as mock_datetime:
+            mock_datetime.utcnow.return_value = frozen_datetime
+            auth(mock_request)
+
+        self.assertEqual({
+            'Authorization': 'AWS4-HMAC-SHA256 Credential=YOURKEY/20160618/us-east-1/es/aws4_request, '
+            'SignedHeaders=host;x-amz-date, '
+            'Signature=0836dae4bce95c1bcdbd3751c84c0c7e589ba7c81331bab92d0e1acb94adcdd9',
+            'Content-Type': 'application/x-www-form-urlencoded',
+            'x-amz-date': '20160618T220405Z',
+
+        }, mock_request.headers)
diff --git a/aws_requests_auth/tests/test_boto_utils.py b/aws_requests_auth/tests/test_boto_utils.py
new file mode 100644
index 0000000..57921e9
--- /dev/null
+++ b/aws_requests_auth/tests/test_boto_utils.py
@@ -0,0 +1,59 @@
+import datetime
+import os
+import unittest
+
+import mock
+
+from aws_requests_auth.aws_auth import AWSRequestsAuth
+from aws_requests_auth.boto_utils import BotoAWSRequestsAuth, get_credentials
+
+
+class TestBotoUtils(unittest.TestCase):
+    """
+    Tests for boto_utils module.
+    """
+
+    def setUp(self):
+        self.saved_env = {}
+        for var in ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN']:
+            self.saved_env[var] = os.environ.get(var)
+            os.environ[var] = 'test-%s' % var
+
+    def tearDown(self):
+        for k, v in self.saved_env.items():
+            if v is None:
+                os.environ.pop(k)
+            else:
+                os.environ[k] = v
+
+    def test_get_credentials(self):
+        creds = get_credentials()  # botocore should discover these from os.environ
+        self.assertEqual(creds['aws_access_key'], 'test-AWS_ACCESS_KEY_ID')
+        self.assertEqual(creds['aws_secret_access_key'], 'test-AWS_SECRET_ACCESS_KEY')
+        self.assertEqual(creds['aws_token'], 'test-AWS_SESSION_TOKEN')
+
+    def test_boto_class(self):
+        boto_auth_inst = BotoAWSRequestsAuth(
+            aws_host='search-foo.us-east-1.es.amazonaws.com',
+            aws_region='us-east-1',
+            aws_service='es',
+        )
+        url = 'http://search-foo.us-east-1.es.amazonaws.com:80/'
+        mock_request = mock.Mock()
+        mock_request.url = url
+        mock_request.method = "GET"
+        mock_request.body = None
+        mock_request.headers = {}
+
+        frozen_datetime = datetime.datetime(2016, 6, 18, 22, 4, 5)
+        with mock.patch('datetime.datetime') as mock_datetime:
+            mock_datetime.utcnow.return_value = frozen_datetime
+            boto_auth_inst(mock_request)
+        self.assertEqual({
+            'Authorization': 'AWS4-HMAC-SHA256 Credential=test-AWS_ACCESS_KEY_ID/20160618/us-east-1/es/aws4_request, '
+                             'SignedHeaders=host;x-amz-date;x-amz-security-token, '
+                             'Signature=9d35f096395c7aa5061e69aca897417dd41bb8fb01a465bb78343624f8f123bf',
+            'x-amz-date': '20160618T220405Z',
+            'X-Amz-Security-Token': 'test-AWS_SESSION_TOKEN'
+
+        }, mock_request.headers)
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..477655d
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,19 @@
+from distutils.core import setup
+
+
+setup(
+    name='aws-requests-auth',
+    version='0.4.1',
+    author='David Muller',
+    author_email='davehmuller at gmail.com',
+    packages=['aws_requests_auth'],
+    url='https://github.com/davidmuller/aws-requests-auth',
+    description='AWS signature version 4 signing process for the python requests module',
+    long_description='See https://github.com/davidmuller/aws-requests-auth for installation and usage instructions.',
+    install_requires=['requests>=0.14.0'],
+    classifiers=[
+        'License :: OSI Approved :: BSD License',
+        'Programming Language :: Python',
+        'Programming Language :: Python :: 3',
+    ]
+)

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-aws-requests-auth.git



More information about the Python-modules-commits mailing list