Skip to content

Commit 059c19f

Browse files
committed
Merge branch 'master' of github.com:mongodb/mongo-python-driver
2 parents e77748e + efe8cc3 commit 059c19f

24 files changed

+1236
-1123
lines changed

.evergreen/config.yml

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -991,6 +991,30 @@ task_groups:
991991
tasks:
992992
- oidc-auth-test-azure-latest
993993

994+
- name: testgcpoidc_task_group
995+
setup_group:
996+
- func: fetch source
997+
- func: prepare resources
998+
- func: fix absolute paths
999+
- func: make files executable
1000+
- command: subprocess.exec
1001+
params:
1002+
binary: bash
1003+
env:
1004+
GCPOIDC_VMNAME_PREFIX: "PYTHON_DRIVER"
1005+
args:
1006+
- ${DRIVERS_TOOLS}/.evergreen/auth_oidc/gcp/setup.sh
1007+
teardown_task:
1008+
- command: subprocess.exec
1009+
params:
1010+
binary: bash
1011+
args:
1012+
- ${DRIVERS_TOOLS}/.evergreen/auth_oidc/gcp/teardown.sh
1013+
setup_group_can_fail_task: true
1014+
setup_group_timeout_secs: 1800
1015+
tasks:
1016+
- oidc-auth-test-gcp-latest
1017+
9941018
- name: testoidc_task_group
9951019
setup_group:
9961020
- func: fetch source
@@ -1966,6 +1990,25 @@ tasks:
19661990
export AZUREOIDC_TEST_CMD="OIDC_ENV=azure ./.evergreen/run-mongodb-oidc-test.sh"
19671991
bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh
19681992
1993+
- name: "oidc-auth-test-gcp-latest"
1994+
commands:
1995+
- command: shell.exec
1996+
params:
1997+
shell: bash
1998+
script: |-
1999+
set -o errexit
2000+
${PREPARE_SHELL}
2001+
cd src
2002+
git add .
2003+
git commit -m "add files"
2004+
export GCPOIDC_DRIVERS_TAR_FILE=/tmp/mongo-python-driver.tgz
2005+
git archive -o $GCPOIDC_DRIVERS_TAR_FILE HEAD
2006+
# Define the command to run on the VM.
2007+
# Ensure that we source the environment file created for us, set up any other variables we need,
2008+
# and then run our test suite on the vm.
2009+
export GCPOIDC_TEST_CMD="OIDC_ENV=gcp ./.evergreen/run-mongodb-oidc-test.sh"
2010+
bash $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/run-driver-test.sh
2011+
19692012
- name: "test-fips-standalone"
19702013
tags: ["fips"]
19712014
commands:
@@ -2995,18 +3038,25 @@ buildvariants:
29953038
- matrix_name: "oidc-auth-test"
29963039
matrix_spec:
29973040
platform: [ rhel8, macos-1100, windows-64-vsMulti-small ]
2998-
display_name: "MONGODB-OIDC Auth ${platform}"
3041+
display_name: "OIDC Auth ${platform}"
29993042
tasks:
30003043
- name: testoidc_task_group
30013044
batchtime: 20160 # 14 days
30023045

30033046
- name: testazureoidc-variant
3004-
display_name: "Azure OIDC"
3005-
run_on: ubuntu2004-small
3047+
display_name: "OIDC Auth Azure"
3048+
run_on: ubuntu2204-small
30063049
tasks:
30073050
- name: testazureoidc_task_group
30083051
batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README
30093052

3053+
- name: testgcpoidc-variant
3054+
display_name: "OIDC Auth GCP"
3055+
run_on: ubuntu2204-small
3056+
tasks:
3057+
- name: testgcpoidc_task_group
3058+
batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README
3059+
30103060
- matrix_name: "aws-auth-test"
30113061
matrix_spec:
30123062
platform: [ubuntu-20.04]

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/bin/bash
22

33
set +x # Disable debug trace
4-
set -o errexit # Exit the script with error if any of the commands fail
4+
set -eu
55

66
echo "Running MONGODB-OIDC authentication tests"
77

@@ -18,6 +18,9 @@ if [ $OIDC_ENV == "test" ]; then
1818
elif [ $OIDC_ENV == "azure" ]; then
1919
source ./env.sh
2020

21+
elif [ $OIDC_ENV == "gcp" ]; then
22+
source ./secrets-export.sh
23+
2124
else
2225
echo "Unrecognized OIDC_ENV $OIDC_ENV"
2326
exit 1

doc/changelog.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ PyMongo 4.7 brings a number of improvements including:
4141
:attr:`pymongo.monitoring.ConnectionReadyEvent.duration` properties.
4242
- Added the ``type`` and ``kwargs`` arguments to :class:`~pymongo.operations.SearchIndexModel` to enable
4343
creating vector search indexes in MongoDB Atlas.
44+
- Fixed a bug where ``read_concern`` and ``write_concern`` were improperly added to
45+
:meth:`~pymongo.collection.Collection.list_search_indexes` queries.
4446

4547

4648
Unavoidable breaking changes

pymongo/_gcp_helpers.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright 2024-present MongoDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""GCP helpers."""
16+
from __future__ import annotations
17+
18+
from typing import Any
19+
from urllib.request import Request, urlopen
20+
21+
22+
def _get_gcp_response(resource: str, timeout: float = 5) -> dict[str, Any]:
23+
url = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity"
24+
url += f"?audience={resource}"
25+
headers = {"Metadata-Flavor": "Google", "Accept": "application/json"}
26+
request = Request(url, headers=headers) # noqa: S310
27+
try:
28+
with urlopen(request, timeout=timeout) as response: # noqa: S310
29+
status = response.status
30+
body = response.read().decode("utf8")
31+
except Exception as e:
32+
msg = "Failed to acquire IMDS access token: %s" % e
33+
raise ValueError(msg) from None
34+
35+
if status != 200:
36+
msg = "Failed to acquire IMDS access token."
37+
raise ValueError(msg)
38+
39+
return dict(access_token=body)

pymongo/auth.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
_authenticate_oidc,
4242
_get_authenticator,
4343
_OIDCAzureCallback,
44+
_OIDCGCPCallback,
4445
_OIDCProperties,
4546
_OIDCTestCallback,
4647
)
@@ -207,6 +208,13 @@ def _build_credentials_tuple(
207208
"Azure environment for MONGODB-OIDC requires a TOKEN_RESOURCE auth mechanism property"
208209
)
209210
callback = _OIDCAzureCallback(token_resource)
211+
elif environ == "gcp":
212+
passwd = None
213+
if not token_resource:
214+
raise ConfigurationError(
215+
"GCP provider for MONGODB-OIDC requires a TOKEN_RESOURCE auth mechanism property"
216+
)
217+
callback = _OIDCGCPCallback(token_resource)
210218
else:
211219
raise ConfigurationError(f"unrecognized ENVIRONMENT for MONGODB-OIDC: {environ}")
212220
else:

pymongo/auth_oidc.py

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
from bson.binary import Binary
2727
from pymongo._azure_helpers import _get_azure_response
2828
from pymongo._csot import remaining
29+
from pymongo._gcp_helpers import _get_gcp_response
2930
from pymongo.errors import ConfigurationError, OperationFailure
31+
from pymongo.helpers import _AUTHENTICATION_FAILURE_CODE
3032

3133
if TYPE_CHECKING:
3234
from pymongo.auth import MongoCredential
@@ -36,7 +38,7 @@
3638
@dataclass
3739
class OIDCIdPInfo:
3840
issuer: str
39-
clientId: str
41+
clientId: Optional[str] = field(default=None)
4042
requestScopes: Optional[list[str]] = field(default=None)
4143

4244

@@ -133,6 +135,15 @@ def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult:
133135
)
134136

135137

138+
class _OIDCGCPCallback(OIDCCallback):
139+
def __init__(self, token_resource: str) -> None:
140+
self.token_resource = token_resource
141+
142+
def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult:
143+
resp = _get_gcp_response(self.token_resource, context.timeout_seconds)
144+
return OIDCCallbackResult(access_token=resp["access_token"])
145+
146+
136147
@dataclass
137148
class _OIDCAuthenticator:
138149
username: str
@@ -179,30 +190,43 @@ def get_spec_auth_cmd(self) -> Optional[MutableMapping[str, Any]]:
179190

180191
def _authenticate_machine(self, conn: Connection) -> Mapping[str, Any]:
181192
# If there is a cached access token, try to authenticate with it. If
182-
# authentication fails, it's possible the cached access token is expired. In
183-
# that case, invalidate the access token, fetch a new access token, and try
184-
# to authenticate again.
193+
# authentication fails with error code 18, invalidate the access token,
194+
# fetch a new access token, and try to authenticate again. If authentication
195+
# fails for any other reason, raise the error to the user.
185196
if self.access_token:
186197
try:
187198
return self._sasl_start_jwt(conn)
188-
except Exception: # noqa: S110
189-
pass
199+
except OperationFailure as e:
200+
if self._is_auth_error(e):
201+
return self._authenticate_machine(conn)
202+
raise
190203
return self._sasl_start_jwt(conn)
191204

192205
def _authenticate_human(self, conn: Connection) -> Optional[Mapping[str, Any]]:
193206
# If we have a cached access token, try a JwtStepRequest.
207+
# authentication fails with error code 18, invalidate the access token,
208+
# and try to authenticate again. If authentication fails for any other
209+
# reason, raise the error to the user.
194210
if self.access_token:
195211
try:
196212
return self._sasl_start_jwt(conn)
197-
except Exception: # noqa: S110
198-
pass
213+
except OperationFailure as e:
214+
if self._is_auth_error(e):
215+
return self._authenticate_human(conn)
216+
raise
199217

200218
# If we have a cached refresh token, try a JwtStepRequest with that.
219+
# If authentication fails with error code 18, invalidate the access and
220+
# refresh tokens, and try to authenticate again. If authentication fails for
221+
# any other reason, raise the error to the user.
201222
if self.refresh_token:
202223
try:
203224
return self._sasl_start_jwt(conn)
204-
except Exception: # noqa: S110
205-
pass
225+
except OperationFailure as e:
226+
if self._is_auth_error(e):
227+
self.refresh_token = None
228+
return self._authenticate_human(conn)
229+
raise
206230

207231
# Start a new Two-Step SASL conversation.
208232
# Run a PrincipalStepRequest to get the IdpInfo.
@@ -270,10 +294,16 @@ def _get_access_token(self) -> Optional[str]:
270294
def _run_command(self, conn: Connection, cmd: MutableMapping[str, Any]) -> Mapping[str, Any]:
271295
try:
272296
return conn.command("$external", cmd, no_reauth=True) # type: ignore[call-arg]
273-
except OperationFailure:
274-
self._invalidate(conn)
297+
except OperationFailure as e:
298+
if self._is_auth_error(e):
299+
self._invalidate(conn)
275300
raise
276301

302+
def _is_auth_error(self, err: Exception) -> bool:
303+
if not isinstance(err, OperationFailure):
304+
return False
305+
return err.code == _AUTHENTICATION_FAILURE_CODE
306+
277307
def _invalidate(self, conn: Connection) -> None:
278308
# Ignore the invalidation if a token gen id is given and is less than our
279309
# current token gen id.

pymongo/collection.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
_IndexList,
7373
_Op,
7474
)
75+
from pymongo.read_concern import DEFAULT_READ_CONCERN, ReadConcern
7576
from pymongo.read_preferences import ReadPreference, _ServerMode
7677
from pymongo.results import (
7778
BulkWriteResult,
@@ -81,7 +82,7 @@
8182
UpdateResult,
8283
)
8384
from pymongo.typings import _CollationIn, _DocumentType, _DocumentTypeArg, _Pipeline
84-
from pymongo.write_concern import WriteConcern, validate_boolean
85+
from pymongo.write_concern import DEFAULT_WRITE_CONCERN, WriteConcern, validate_boolean
8586

8687
T = TypeVar("T")
8788

@@ -119,7 +120,6 @@ class ReturnDocument:
119120
from pymongo.collation import Collation
120121
from pymongo.database import Database
121122
from pymongo.pool import Connection
122-
from pymongo.read_concern import ReadConcern
123123
from pymongo.server import Server
124124

125125

@@ -2364,7 +2364,10 @@ def list_search_indexes(
23642364
pipeline = [{"$listSearchIndexes": {"name": name}}]
23652365

23662366
coll = self.with_options(
2367-
codec_options=DEFAULT_CODEC_OPTIONS, read_preference=ReadPreference.PRIMARY
2367+
codec_options=DEFAULT_CODEC_OPTIONS,
2368+
read_preference=ReadPreference.PRIMARY,
2369+
write_concern=DEFAULT_WRITE_CONCERN,
2370+
read_concern=DEFAULT_READ_CONCERN,
23682371
)
23692372
cmd = _CollectionAggregationCommand(
23702373
coll,

pymongo/common.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,6 @@ def validate_read_preference_tags(name: str, value: Any) -> list[dict[str, str]]
426426
"AWS_SESSION_TOKEN",
427427
"ENVIRONMENT",
428428
"TOKEN_RESOURCE",
429-
"ALLOWED_HOSTS",
430429
]
431430
)
432431

pymongo/helpers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@
9090
# Server code raised when re-authentication is required
9191
_REAUTHENTICATION_REQUIRED_CODE: int = 391
9292

93+
# Server code raised when authentication fails.
94+
_AUTHENTICATION_FAILURE_CODE: int = 18
95+
9396

9497
def _gen_index_name(keys: _IndexList) -> str:
9598
"""Generate an index name from the set of fields it is over."""

pymongo/monitor.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,12 @@ def __init__(self, topology: Topology, topology_settings: TopologySettings):
335335
self._seedlist = self._settings._seeds
336336
assert isinstance(self._settings.fqdn, str)
337337
self._fqdn: str = self._settings.fqdn
338+
self._startup_time = time.monotonic()
338339

339340
def _run(self) -> None:
341+
# Don't poll right after creation, wait 60 seconds first
342+
if time.monotonic() < self._startup_time + common.MIN_SRV_RESCAN_INTERVAL:
343+
return
340344
seedlist = self._get_seedlist()
341345
if seedlist:
342346
self._seedlist = seedlist

0 commit comments

Comments
 (0)