Skip to content

Commit 1e0ef67

Browse files
authored
PYTHON-3664 OIDC: Automatic token acquisition for GCP Identity Provider (#1540)
1 parent c154c6b commit 1e0ef67

File tree

8 files changed

+155
-9
lines changed

8 files changed

+155
-9
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

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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
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
3031

3132
if TYPE_CHECKING:
@@ -133,6 +134,15 @@ def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult:
133134
)
134135

135136

137+
class _OIDCGCPCallback(OIDCCallback):
138+
def __init__(self, token_resource: str) -> None:
139+
self.token_resource = token_resource
140+
141+
def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult:
142+
resp = _get_gcp_response(self.token_resource, context.timeout_seconds)
143+
return OIDCCallbackResult(access_token=resp["access_token"])
144+
145+
136146
@dataclass
137147
class _OIDCAuthenticator:
138148
username: str

test/auth/legacy/connection-string.json

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,10 +540,37 @@
540540
"credential": null
541541
},
542542
{
543-
"description": "should throw and exception if no token audience is given for azure provider (MONGODB-OIDC)",
543+
"description": "should throw an exception if no token audience is given for azure provider (MONGODB-OIDC)",
544544
"uri": "mongodb://username@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure",
545545
"valid": false,
546546
"credential": null
547+
},
548+
{
549+
"description": "should recognise the mechanism with gcp provider (MONGODB-OIDC)",
550+
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:foo",
551+
"valid": true,
552+
"credential": {
553+
"username": null,
554+
"password": null,
555+
"source": "$external",
556+
"mechanism": "MONGODB-OIDC",
557+
"mechanism_properties": {
558+
"ENVIRONMENT": "gcp",
559+
"TOKEN_RESOURCE": "foo"
560+
}
561+
}
562+
},
563+
{
564+
"description": "should throw an error for a username and password with gcp provider (MONGODB-OIDC)",
565+
"uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:foo",
566+
"valid": false,
567+
"credential": null
568+
},
569+
{
570+
"description": "should throw an error if not TOKEN_RESOURCE with gcp provider (MONGODB-OIDC)",
571+
"uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp",
572+
"valid": false,
573+
"credential": null
547574
}
548575
]
549576
}

test/auth_oidc/test_auth_oidc.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@
2727

2828
sys.path[0:0] = [""]
2929

30+
import pprint
3031
from test.unified_format import generate_test_classes
3132
from test.utils import EventListener
3233

3334
from bson import SON
3435
from pymongo import MongoClient
3536
from pymongo._azure_helpers import _get_azure_response
37+
from pymongo._gcp_helpers import _get_gcp_response
3638
from pymongo.auth_oidc import (
3739
OIDCCallback,
3840
OIDCCallbackResult,
@@ -75,10 +77,12 @@ def get_token(self, username=None):
7577
return fid.read()
7678
elif ENVIRON == "azure":
7779
opts = parse_uri(self.uri_single)["options"]
78-
resource = opts["authmechanismproperties"]["TOKEN_RESOURCE"]
79-
return _get_azure_response(resource, username)["access_token"]
80-
else:
81-
raise RuntimeError(f"Invalid ENVIRONMENT {ENVIRON}")
80+
token_aud = opts["authmechanismproperties"]["TOKEN_RESOURCE"]
81+
return _get_azure_response(token_aud, username)["access_token"]
82+
elif ENVIRON == "gcp":
83+
opts = parse_uri(self.uri_single)["options"]
84+
token_aud = opts["authmechanismproperties"]["TOKEN_RESOURCE"]
85+
return _get_gcp_response(token_aud, username)["access_token"]
8286

8387
@contextmanager
8488
def fail_point(self, command_args):

test/unified_format.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,11 @@
172172
"ENVIRONMENT": "azure",
173173
"TOKEN_RESOURCE": os.environ["AZUREOIDC_RESOURCE"],
174174
}
175+
elif OIDC_ENV == "gcp":
176+
PLACEHOLDER_MAP["/uriOptions/authMechanismProperties"] = {
177+
"ENVIRONMENT": "gcp",
178+
"TOKEN_RESOURCE": os.environ["GCPOIDC_AUDIENCE"],
179+
}
175180

176181

177182
def interrupt_loop():

0 commit comments

Comments
 (0)