Skip to content

Commit 903643b

Browse files
committed
PYTHON-2138 Use pymongo-auth-aws for MONGODB-AWS support
1 parent 719b025 commit 903643b

File tree

7 files changed

+50
-186
lines changed

7 files changed

+50
-186
lines changed

.evergreen/run-mongodb-aws-ecs-test.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ authtest () {
3535

3636
$VIRTUALENV -p $PYTHON --system-site-packages --never-download venvaws
3737
. venvaws/bin/activate
38-
pip install requests botocore
3938

4039
cd src
40+
pip install '.[aws]'
4141
python test/auth_aws/test_auth_aws.py
4242
cd -
4343
deactivate

.evergreen/run-mongodb-aws-test.sh

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@ authtest () {
5555
else
5656
. venvaws/bin/activate
5757
fi
58-
pip install requests botocore
59-
58+
pip install '.[aws]'
6059
python test/auth_aws/test_auth_aws.py
6160
deactivate
6261
rm -rf venvaws

README.rst

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,8 @@ dependency can be installed automatically along with PyMongo::
100100

101101
$ python -m pip install pymongo[gssapi]
102102

103-
MONGODB-AWS authentication requires `botocore
104-
<https://pypi.org/project/botocore/>`_ and `requests
105-
<https://pypi.org/project/requests/>`_::
103+
MONGODB-AWS authentication requires `pymongo-auth-aws
104+
<https://pypi.org/project/pymongo-auth-aws/>`_::
106105

107106
$ python -m pip install pymongo[aws]
108107

doc/installation.rst

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,8 @@ dependency can be installed automatically along with PyMongo::
5656

5757
$ python -m pip install pymongo[gssapi]
5858

59-
:ref:`MONGODB-AWS` authentication requires `botocore
60-
<https://pypi.org/project/botocore/>`_ and `requests
61-
<https://pypi.org/project/requests/>`_::
59+
:ref:`MONGODB-AWS` authentication requires `pymongo-auth-aws
60+
<https://pypi.org/project/pymongo-auth-aws/>`_::
6261

6362
$ python -m pip install pymongo[aws]
6463

pymongo/auth.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
from bson.binary import Binary
4444
from bson.py3compat import string_type, _unicode, PY3
4545
from bson.son import SON
46-
from pymongo.auth_aws import _HAVE_MONGODB_AWS, _auth_aws, _AWSCredential
46+
from pymongo.auth_aws import _authenticate_aws
4747
from pymongo.errors import ConfigurationError, OperationFailure
4848
from pymongo.saslprep import saslprep
4949

@@ -540,20 +540,6 @@ def _authenticate_x509(credentials, sock_info):
540540
sock_info.command('$external', cmd)
541541

542542

543-
def _authenticate_aws(credentials, sock_info):
544-
"""Authenticate using MONGODB-AWS.
545-
"""
546-
if not _HAVE_MONGODB_AWS:
547-
raise ConfigurationError(
548-
"MONGODB-AWS authentication requires botocore and requests: "
549-
"install these libraries with: "
550-
"python -m pip install 'pymongo[aws]'")
551-
552-
_auth_aws(_AWSCredential(
553-
credentials.username, credentials.password,
554-
credentials.mechanism_properties.aws_session_token), sock_info)
555-
556-
557543
def _authenticate_mongo_cr(credentials, sock_info):
558544
"""Authenticate using MONGODB-CR.
559545
"""

pymongo/auth_aws.py

Lines changed: 42 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -14,188 +14,69 @@
1414

1515
"""MONGODB-AWS Authentication helpers."""
1616

17-
import os
18-
1917
try:
20-
21-
from botocore.auth import SigV4Auth
22-
from botocore.awsrequest import AWSRequest
23-
from botocore.credentials import Credentials
24-
25-
import requests
26-
18+
import pymongo_auth_aws
19+
from pymongo_auth_aws import (AwsCredential,
20+
AwsSaslContext,
21+
PyMongoAuthAwsError)
2722
_HAVE_MONGODB_AWS = True
2823
except ImportError:
2924
_HAVE_MONGODB_AWS = False
3025

3126
import bson
32-
33-
34-
from base64 import standard_b64encode
35-
from collections import namedtuple
36-
3727
from bson.binary import Binary
3828
from bson.son import SON
3929
from pymongo.errors import ConfigurationError, OperationFailure
4030

4131

42-
_AWS_REL_URI = 'http://169.254.170.2/'
43-
_AWS_EC2_URI = 'http://169.254.169.254/'
44-
_AWS_EC2_PATH = 'latest/meta-data/iam/security-credentials/'
45-
_AWS_HTTP_TIMEOUT = 10
46-
47-
48-
_AWSCredential = namedtuple('_AWSCredential',
49-
['username', 'password', 'token'])
50-
"""MONGODB-AWS credentials."""
51-
52-
53-
def _aws_temp_credentials():
54-
"""Construct temporary MONGODB-AWS credentials."""
55-
access_key = os.environ.get('AWS_ACCESS_KEY_ID')
56-
secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY')
57-
if access_key and secret_key:
58-
return _AWSCredential(
59-
access_key, secret_key, os.environ.get('AWS_SESSION_TOKEN'))
60-
# If the environment variable
61-
# AWS_CONTAINER_CREDENTIALS_RELATIVE_URI is set then drivers MUST
62-
# assume that it was set by an AWS ECS agent and use the URI
63-
# http://169.254.170.2/$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI to
64-
# obtain temporary credentials.
65-
relative_uri = os.environ.get('AWS_CONTAINER_CREDENTIALS_RELATIVE_URI')
66-
if relative_uri is not None:
67-
try:
68-
res = requests.get(_AWS_REL_URI+relative_uri,
69-
timeout=_AWS_HTTP_TIMEOUT)
70-
res_json = res.json()
71-
except (ValueError, requests.exceptions.RequestException):
72-
raise OperationFailure(
73-
'temporary MONGODB-AWS credentials could not be obtained')
74-
else:
75-
# If the environment variable AWS_CONTAINER_CREDENTIALS_RELATIVE_URI is
76-
# not set drivers MUST assume we are on an EC2 instance and use the
77-
# endpoint
78-
# http://169.254.169.254/latest/meta-data/iam/security-credentials
79-
# /<role-name>
80-
# whereas role-name can be obtained from querying the URI
81-
# http://169.254.169.254/latest/meta-data/iam/security-credentials/.
82-
try:
83-
# Get token
84-
headers = {'X-aws-ec2-metadata-token-ttl-seconds': "30"}
85-
res = requests.post(_AWS_EC2_URI+'latest/api/token',
86-
headers=headers, timeout=_AWS_HTTP_TIMEOUT)
87-
token = res.content
88-
# Get role name
89-
headers = {'X-aws-ec2-metadata-token': token}
90-
res = requests.get(_AWS_EC2_URI+_AWS_EC2_PATH, headers=headers,
91-
timeout=_AWS_HTTP_TIMEOUT)
92-
role = res.text
93-
# Get temp creds
94-
res = requests.get(_AWS_EC2_URI+_AWS_EC2_PATH+role,
95-
headers=headers, timeout=_AWS_HTTP_TIMEOUT)
96-
res_json = res.json()
97-
except (ValueError, requests.exceptions.RequestException):
98-
raise OperationFailure(
99-
'temporary MONGODB-AWS credentials could not be obtained')
100-
101-
try:
102-
temp_user = res_json['AccessKeyId']
103-
temp_password = res_json['SecretAccessKey']
104-
token = res_json['Token']
105-
except KeyError:
106-
# If temporary credentials cannot be obtained then drivers MUST
107-
# fail authentication and raise an error.
108-
raise OperationFailure(
109-
'temporary MONGODB-AWS credentials could not be obtained')
110-
111-
return _AWSCredential(temp_user, temp_password, token)
112-
113-
114-
_AWS4_HMAC_SHA256 = 'AWS4-HMAC-SHA256'
115-
_AWS_SERVICE = 'sts'
116-
32+
class _AwsSaslContext(AwsSaslContext):
33+
# Dependency injection:
34+
def binary_type(self):
35+
"""Return the bson.binary.Binary type."""
36+
return Binary
11737

118-
def _get_region(sts_host):
119-
""""""
120-
parts = sts_host.split('.')
121-
if len(parts) == 1 or sts_host == 'sts.amazonaws.com':
122-
return 'us-east-1' # Default
38+
def bson_encode(self, doc):
39+
"""Encode a dictionary to BSON."""
40+
return bson.encode(doc)
12341

124-
if len(parts) > 2 or not all(parts):
125-
raise OperationFailure("Server returned an invalid sts host")
42+
def bson_decode(self, data):
43+
"""Decode BSON to a dictionary."""
44+
return bson.decode(data)
12645

127-
return parts[1]
128-
129-
130-
def _aws_auth_header(credentials, server_nonce, sts_host):
131-
"""Signature Version 4 Signing Process to construct the authorization header
132-
"""
133-
region = _get_region(sts_host)
13446

135-
request_parameters = 'Action=GetCallerIdentity&Version=2011-06-15'
136-
encoded_nonce = standard_b64encode(server_nonce).decode('utf8')
137-
request_headers = {
138-
'Content-Type': 'application/x-www-form-urlencoded',
139-
'Content-Length': str(len(request_parameters)),
140-
'Host': sts_host,
141-
'X-MongoDB-Server-Nonce': encoded_nonce,
142-
'X-MongoDB-GS2-CB-Flag': 'n',
143-
}
144-
request = AWSRequest(method="POST", url="/", data=request_parameters,
145-
headers=request_headers)
146-
boto_creds = Credentials(credentials.username, credentials.password,
147-
token=credentials.token)
148-
auth = SigV4Auth(boto_creds, "sts", region)
149-
auth.add_auth(request)
150-
final = {
151-
'a': request.headers['Authorization'],
152-
'd': request.headers['X-Amz-Date']
153-
}
154-
if credentials.token:
155-
final['t'] = credentials.token
156-
return final
157-
158-
159-
def _auth_aws(credentials, sock_info):
47+
def _authenticate_aws(credentials, sock_info):
16048
"""Authenticate using MONGODB-AWS.
16149
"""
16250
if not _HAVE_MONGODB_AWS:
16351
raise ConfigurationError(
164-
"MONGODB-AWS authentication requires botocore and requests: "
165-
"install these libraries with: "
166-
"python -m pip install 'pymongo[aws]'")
52+
"MONGODB-AWS authentication requires pymongo-auth-aws: "
53+
"install with: python -m pip install 'pymongo[aws]'")
16754

16855
if sock_info.max_wire_version < 9:
16956
raise ConfigurationError(
17057
"MONGODB-AWS authentication requires MongoDB version 4.4 or later")
17158

172-
# If a username and password are not provided, drivers MUST query
173-
# a link-local AWS address for temporary credentials.
174-
if credentials.username is None:
175-
credentials = _aws_temp_credentials()
176-
177-
# Client first.
178-
client_nonce = os.urandom(32)
179-
payload = {'r': Binary(client_nonce), 'p': 110}
180-
client_first = SON([('saslStart', 1),
181-
('mechanism', 'MONGODB-AWS'),
182-
('payload', Binary(bson.encode(payload)))])
183-
server_first = sock_info.command('$external', client_first)
184-
185-
server_payload = bson.decode(server_first['payload'])
186-
server_nonce = server_payload['s']
187-
if len(server_nonce) != 64 or not server_nonce.startswith(client_nonce):
188-
raise OperationFailure("Server returned an invalid nonce.")
189-
sts_host = server_payload['h']
190-
if len(sts_host) < 1 or len(sts_host) > 255 or '..' in sts_host:
191-
# Drivers must also validate that the host is greater than 0 and less
192-
# than or equal to 255 bytes per RFC 1035.
193-
raise OperationFailure("Server returned an invalid sts host.")
194-
195-
payload = _aws_auth_header(credentials, server_nonce, sts_host)
196-
client_second = SON([('saslContinue', 1),
197-
('conversationId', server_first['conversationId']),
198-
('payload', Binary(bson.encode(payload)))])
199-
res = sock_info.command('$external', client_second)
200-
if not res['done']:
201-
raise OperationFailure('MONGODB-AWS conversation failed to complete.')
59+
try:
60+
ctx = _AwsSaslContext(AwsCredential(
61+
credentials.username, credentials.password,
62+
credentials.mechanism_properties.aws_session_token))
63+
client_payload = ctx.step(None)
64+
client_first = SON([('saslStart', 1),
65+
('mechanism', 'MONGODB-AWS'),
66+
('payload', client_payload)])
67+
server_first = sock_info.command('$external', client_first)
68+
res = server_first
69+
# Limit how many times we loop to catch protocol / library issues
70+
for _ in range(10):
71+
client_payload = ctx.step(res['payload'])
72+
cmd = SON([('saslContinue', 1),
73+
('conversationId', server_first['conversationId']),
74+
('payload', client_payload)])
75+
res = sock_info.command('$external', cmd)
76+
if res['done']:
77+
# SASL complete.
78+
break
79+
except PyMongoAuthAwsError as exc:
80+
# Convert to OperationFailure and include pymongo-auth-aws version.
81+
raise OperationFailure('%s (pymongo-auth-aws version %s)' % (
82+
exc, pymongo_auth_aws.__version__))

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ def build_extension(self, ext):
329329
'snappy': ['python-snappy'],
330330
'tls': [],
331331
'zstd': ['zstandard'],
332-
'aws': ['requests<3.0.0', 'botocore'],
332+
'aws': ['pymongo-auth-aws<2.0.0'],
333333
}
334334

335335
# https://jira.mongodb.org/browse/PYTHON-2117

0 commit comments

Comments
 (0)