-
Notifications
You must be signed in to change notification settings - Fork 183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added support for AWS Sigv4 for UrlLib3. #547
Changes from 9 commits
a732d44
e18b703
fd9fa73
6500c2b
f8f2b62
ae1ebe1
df5b29b
8d5aeb0
ee4517d
cb76874
9e6b9aa
e916060
284a5fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
# GitHub history for details. | ||
|
||
import sys | ||
from typing import Any, Callable, Dict | ||
|
||
import requests | ||
|
||
|
@@ -43,12 +44,12 @@ def fetch_url(prepared_request): # type: ignore | |
return url.scheme + "://" + location + path + querystring | ||
|
||
|
||
class AWSV4SignerAuth(requests.auth.AuthBase): | ||
class AWSV4Signer: | ||
""" | ||
AWS V4 Request Signer for Requests. | ||
Generic AWS V4 Request Signer. | ||
""" | ||
|
||
def __init__(self, credentials, region, service="es"): # type: ignore | ||
def __init__(self, credentials, region: str, service: str = "es") -> Any: # type: ignore | ||
if not credentials: | ||
raise ValueError("Credentials cannot be empty") | ||
self.credentials = credentials | ||
|
@@ -61,27 +62,20 @@ def __init__(self, credentials, region, service="es"): # type: ignore | |
raise ValueError("Service name cannot be empty") | ||
self.service = service | ||
|
||
def __call__(self, request): # type: ignore | ||
return self._sign_request(request) # type: ignore | ||
|
||
def _sign_request(self, prepared_request): # type: ignore | ||
def sign(self, method: str, url: str, body: Any) -> Dict[str, str]: | ||
""" | ||
This method helps in signing the request by injecting the required headers. | ||
:param prepared_request: unsigned request | ||
:return: signed request | ||
This method signs the request and returns headers. | ||
:param method: HTTP method | ||
:param url: url | ||
:param body: body | ||
:return: headers | ||
""" | ||
|
||
from botocore.auth import SigV4Auth | ||
from botocore.awsrequest import AWSRequest | ||
|
||
url = fetch_url(prepared_request) # type: ignore | ||
|
||
# create an AWS request object and sign it using SigV4Auth | ||
aws_request = AWSRequest( | ||
method=prepared_request.method.upper(), | ||
url=url, | ||
data=prepared_request.body, | ||
) | ||
aws_request = AWSRequest(method=method.upper(), url=url, data=body) | ||
|
||
# credentials objects expose access_key, secret_key and token attributes | ||
# via @property annotations that call _refresh() on every access, | ||
|
@@ -101,9 +95,49 @@ def _sign_request(self, prepared_request): # type: ignore | |
sig_v4_auth.add_auth(aws_request) | ||
|
||
# copy the headers from AWS request object into the prepared_request | ||
prepared_request.headers.update(dict(aws_request.headers.items())) | ||
prepared_request.headers["X-Amz-Content-SHA256"] = sig_v4_auth.payload( | ||
aws_request | ||
headers = dict(aws_request.headers.items()) | ||
headers["X-Amz-Content-SHA256"] = sig_v4_auth.payload(aws_request) | ||
|
||
return headers | ||
|
||
|
||
class RequestsAWSV4SignerAuth(requests.auth.AuthBase): | ||
""" | ||
AWS V4 Request Signer for Requests. | ||
""" | ||
|
||
def __init__(self, credentials, region, service="es"): # type: ignore | ||
self.signer = AWSV4Signer(credentials, region, service) | ||
|
||
def __call__(self, request): # type: ignore | ||
return self._sign_request(request) # type: ignore | ||
|
||
def _sign_request(self, prepared_request): # type: ignore | ||
""" | ||
This method helps in signing the request by injecting the required headers. | ||
:param prepared_request: unsigned request | ||
:return: signed request | ||
""" | ||
|
||
prepared_request.headers.update( | ||
self.signer.sign( | ||
prepared_request.method, | ||
fetch_url(prepared_request), # type: ignore | ||
prepared_request.body, | ||
) | ||
) | ||
|
||
return prepared_request | ||
|
||
|
||
# Deprecated: use RequestsAWSV4SignerAuth | ||
class AWSV4SignerAuth(RequestsAWSV4SignerAuth): | ||
pass | ||
|
||
|
||
class Urllib3AWSV4SignerAuth(Callable): # type: ignore | ||
def __init__(self, credentials, region, service="es"): # type: ignore | ||
self.signer = AWSV4Signer(credentials, region, service) | ||
|
||
def __call__(self, method: str, url: str, body: Any) -> Dict[str, str]: | ||
return self.signer.sign(method, url, body) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The The urllib3 library doesn't support such an interface. It actually splits the client constructor and exposes a We could make a similar interface to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense, thank you! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
## AWS SigV4 Samples | ||
|
||
Create an OpenSearch domain in (AWS) which support IAM based AuthN/AuthZ. | ||
|
||
``` | ||
export AWS_ACCESS_KEY_ID= | ||
export AWS_SECRET_ACCESS_KEY= | ||
export AWS_SESSION_TOKEN= | ||
export AWS_REGION=us-west-2 | ||
|
||
export SERVICE=es # use "aoss" for OpenSearch Serverless. | ||
export ENDPOINT=https://....us-west-2.es.amazonaws.com | ||
|
||
poetry run aws/search-urllib.py | ||
``` | ||
|
||
This will output the version of OpenSearch and a search result. | ||
|
||
``` | ||
opensearch: 2.3.0 | ||
{'director': 'Bennett Miller', 'title': 'Moneyball', 'year': 2011} | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
# SPDX-License-Identifier: Apache-2.0 | ||
# | ||
# The OpenSearch Contributors require contributions made to | ||
# this file be licensed under the Apache-2.0 license or a | ||
# compatible open source license. | ||
# | ||
# Modifications Copyright OpenSearch Contributors. See | ||
# GitHub history for details. | ||
|
||
import logging | ||
|
||
from os import environ | ||
from time import sleep | ||
from urllib.parse import urlparse | ||
|
||
from boto3 import Session | ||
from opensearchpy import RequestsAWSV4SignerAuth, OpenSearch, RequestsHttpConnection | ||
|
||
# verbose logging | ||
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO) | ||
|
||
# cluster endpoint, for example: my-test-domain.us-east-1.es.amazonaws.com | ||
url = urlparse(environ['ENDPOINT']) | ||
region = environ.get('AWS_REGION', 'us-east-1') | ||
service = environ.get('SERVICE', 'es') | ||
|
||
credentials = Session().get_credentials() | ||
|
||
auth = RequestsAWSV4SignerAuth(credentials, region, service) | ||
|
||
client = OpenSearch( | ||
hosts=[{ | ||
'host': url.netloc, | ||
'port': url.port or 443 | ||
}], | ||
http_auth=auth, | ||
use_ssl=True, | ||
verify_certs=True, | ||
connection_class=RequestsHttpConnection, | ||
timeout=30 | ||
) | ||
|
||
# TODO: remove when OpenSearch Serverless adds support for / | ||
if service == 'es': | ||
info = client.info() | ||
print(f"{info['version']['distribution']}: {info['version']['number']}") | ||
|
||
# create an index | ||
index = 'movies' | ||
client.indices.create(index=index) | ||
|
||
try: | ||
# index data | ||
document = {'director': 'Bennett Miller', 'title': 'Moneyball', 'year': 2011} | ||
client.index(index=index, body=document, id='1') | ||
|
||
# wait for the document to index | ||
sleep(1) | ||
|
||
# search for the document | ||
results = client.search(body={'query': {'match': {'director': 'miller'}}}) | ||
for hit in results['hits']['hits']: | ||
print(hit['_source']) | ||
|
||
# delete the document | ||
client.delete(index=index, id='1') | ||
finally: | ||
# delete the index | ||
client.indices.delete(index=index) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we recommend
Urllib3AWSV4SignerAuth
here?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've clarified the documentation in cb76874.