1# Copyright: (c) 2018, Aaron Haaf <aabonh@gmail.com>
2# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
3
4from __future__ import (absolute_import, division, print_function)
5__metaclass__ = type
6
7import datetime
8import hashlib
9import hmac
10import operator
11
12try:
13    from boto3 import session
14except ImportError:
15    pass
16
17from ansible.module_utils.six.moves.urllib.parse import urlencode
18from ansible.module_utils.urls import open_url
19
20from .ec2 import HAS_BOTO3
21from .ec2 import get_aws_connection_info
22
23
24def hexdigest(s):
25    """
26    Returns the sha256 hexdigest of a string after encoding.
27    """
28
29    return hashlib.sha256(s.encode("utf-8")).hexdigest()
30
31
32def format_querystring(params=None):
33    """
34    Returns properly url-encoded query string from the provided params dict.
35
36    It's specially sorted for cannonical requests
37    """
38
39    if not params:
40        return ""
41
42    # Query string values must be URL-encoded (space=%20). The parameters must be sorted by name.
43    return urlencode(sorted(params.items(), operator.itemgetter(0)))
44
45
46# Key derivation functions. See:
47# http://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html#signature-v4-examples-python
48def sign(key, msg):
49    '''
50    Return digest for key applied to msg
51    '''
52
53    return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
54
55
56def get_signature_key(key, dateStamp, regionName, serviceName):
57    '''
58    Returns signature key for AWS resource
59    '''
60
61    kDate = sign(("AWS4" + key).encode("utf-8"), dateStamp)
62    kRegion = sign(kDate, regionName)
63    kService = sign(kRegion, serviceName)
64    kSigning = sign(kService, "aws4_request")
65    return kSigning
66
67
68def get_aws_credentials_object(module):
69    '''
70    Returns aws_access_key_id, aws_secret_access_key, session_token for a module.
71    '''
72
73    if not HAS_BOTO3:
74        module.fail_json("get_aws_credentials_object requires boto3")
75
76    dummy, dummy, boto_params = get_aws_connection_info(module, boto3=True)
77    s = session.Session(**boto_params)
78
79    return s.get_credentials()
80
81
82# Reference: https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html
83def signed_request(
84        module=None,
85        method="GET", service=None, host=None, uri=None,
86        query=None, body="", headers=None,
87        session_in_header=True, session_in_query=False
88):
89    """Generate a SigV4 request to an AWS resource for a module
90
91    This is used if you wish to authenticate with AWS credentials to a secure endpoint like an elastisearch domain.
92
93    Returns :class:`HTTPResponse` object.
94
95    Example:
96        result = signed_request(
97            module=this,
98            service="es",
99            host="search-recipes1-xxxxxxxxx.us-west-2.es.amazonaws.com",
100        )
101
102    :kwarg host: endpoint to talk to
103    :kwarg service: AWS id of service (like `ec2` or `es`)
104    :kwarg module: An AnsibleAWSModule to gather connection info from
105
106    :kwarg body: (optional) Payload to send
107    :kwarg method: (optional) HTTP verb to use
108    :kwarg query: (optional) dict of query params to handle
109    :kwarg uri: (optional) Resource path without query parameters
110
111    :kwarg session_in_header: (optional) Add the session token to the headers
112    :kwarg session_in_query: (optional) Add the session token to the query parameters
113
114    :returns: HTTPResponse
115    """
116
117    if not HAS_BOTO3:
118        module.fail_json("A sigv4 signed_request requires boto3")
119
120    # "Constants"
121
122    t = datetime.datetime.utcnow()
123    amz_date = t.strftime("%Y%m%dT%H%M%SZ")
124    datestamp = t.strftime("%Y%m%d")  # Date w/o time, used in credential scope
125    algorithm = "AWS4-HMAC-SHA256"
126
127    # AWS stuff
128
129    region, dummy, dummy = get_aws_connection_info(module, boto3=True)
130    credentials = get_aws_credentials_object(module)
131    access_key = credentials.access_key
132    secret_key = credentials.secret_key
133    session_token = credentials.token
134
135    if not access_key:
136        module.fail_json(msg="aws_access_key_id is missing")
137    if not secret_key:
138        module.fail_json(msg="aws_secret_access_key is missing")
139
140    credential_scope = "/".join([datestamp, region, service, "aws4_request"])
141
142    # Argument Defaults
143
144    uri = uri or "/"
145    query_string = format_querystring(query) if query else ""
146
147    headers = headers or dict()
148    query = query or dict()
149
150    headers.update({
151        "host": host,
152        "x-amz-date": amz_date,
153    })
154
155    # Handle adding of session_token if present
156    if session_token:
157        if session_in_header:
158            headers["X-Amz-Security-Token"] = session_token
159        if session_in_query:
160            query["X-Amz-Security-Token"] = session_token
161
162    if method == "GET":
163        body = ""
164
165    # Derived data
166
167    body_hash = hexdigest(body)
168    signed_headers = ";".join(sorted(headers.keys()))
169
170    # Setup Cannonical request to generate auth token
171
172    cannonical_headers = "\n".join([
173        key.lower().strip() + ":" + value for key, value in headers.items()
174    ]) + "\n"  # Note additional trailing newline
175
176    cannonical_request = "\n".join([
177        method,
178        uri,
179        query_string,
180        cannonical_headers,
181        signed_headers,
182        body_hash,
183    ])
184
185    string_to_sign = "\n".join([algorithm, amz_date, credential_scope, hexdigest(cannonical_request)])
186
187    # Sign the Cannonical request
188
189    signing_key = get_signature_key(secret_key, datestamp, region, service)
190    signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
191
192    # Make auth header with that info
193
194    authorization_header = "{0} Credential={1}/{2}, SignedHeaders={3}, Signature={4}".format(
195        algorithm, access_key, credential_scope, signed_headers, signature
196    )
197
198    # PERFORM THE REQUEST!
199
200    url = "https://" + host + uri
201
202    if query_string != "":
203        url = url + "?" + query_string
204
205    final_headers = {
206        "x-amz-date": amz_date,
207        "Authorization": authorization_header,
208    }
209
210    final_headers.update(headers)
211
212    return open_url(url, method=method, data=body, headers=final_headers)
213