PYTHON-3664 OIDC: Automatic token acquisition for GCP Identity Provider (#1540)

This commit is contained in:
Steven Silvester 2024-04-03 16:07:41 -05:00 committed by GitHub
parent c154c6b67b
commit 1e0ef67ab8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 155 additions and 9 deletions

View File

@ -991,6 +991,30 @@ task_groups:
tasks:
- oidc-auth-test-azure-latest
- name: testgcpoidc_task_group
setup_group:
- func: fetch source
- func: prepare resources
- func: fix absolute paths
- func: make files executable
- command: subprocess.exec
params:
binary: bash
env:
GCPOIDC_VMNAME_PREFIX: "PYTHON_DRIVER"
args:
- ${DRIVERS_TOOLS}/.evergreen/auth_oidc/gcp/setup.sh
teardown_task:
- command: subprocess.exec
params:
binary: bash
args:
- ${DRIVERS_TOOLS}/.evergreen/auth_oidc/gcp/teardown.sh
setup_group_can_fail_task: true
setup_group_timeout_secs: 1800
tasks:
- oidc-auth-test-gcp-latest
- name: testoidc_task_group
setup_group:
- func: fetch source
@ -1966,6 +1990,25 @@ tasks:
export AZUREOIDC_TEST_CMD="OIDC_ENV=azure ./.evergreen/run-mongodb-oidc-test.sh"
bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh
- name: "oidc-auth-test-gcp-latest"
commands:
- command: shell.exec
params:
shell: bash
script: |-
set -o errexit
${PREPARE_SHELL}
cd src
git add .
git commit -m "add files"
export GCPOIDC_DRIVERS_TAR_FILE=/tmp/mongo-python-driver.tgz
git archive -o $GCPOIDC_DRIVERS_TAR_FILE HEAD
# Define the command to run on the VM.
# Ensure that we source the environment file created for us, set up any other variables we need,
# and then run our test suite on the vm.
export GCPOIDC_TEST_CMD="OIDC_ENV=gcp ./.evergreen/run-mongodb-oidc-test.sh"
bash $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/run-driver-test.sh
- name: "test-fips-standalone"
tags: ["fips"]
commands:
@ -2995,18 +3038,25 @@ buildvariants:
- matrix_name: "oidc-auth-test"
matrix_spec:
platform: [ rhel8, macos-1100, windows-64-vsMulti-small ]
display_name: "MONGODB-OIDC Auth ${platform}"
display_name: "OIDC Auth ${platform}"
tasks:
- name: testoidc_task_group
batchtime: 20160 # 14 days
- name: testazureoidc-variant
display_name: "Azure OIDC"
run_on: ubuntu2004-small
display_name: "OIDC Auth Azure"
run_on: ubuntu2204-small
tasks:
- name: testazureoidc_task_group
batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README
- name: testgcpoidc-variant
display_name: "OIDC Auth GCP"
run_on: ubuntu2204-small
tasks:
- name: testgcpoidc_task_group
batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README
- matrix_name: "aws-auth-test"
matrix_spec:
platform: [ubuntu-20.04]

View File

@ -1,7 +1,7 @@
#!/bin/bash
set +x # Disable debug trace
set -o errexit # Exit the script with error if any of the commands fail
set -eu
echo "Running MONGODB-OIDC authentication tests"
@ -18,6 +18,9 @@ if [ $OIDC_ENV == "test" ]; then
elif [ $OIDC_ENV == "azure" ]; then
source ./env.sh
elif [ $OIDC_ENV == "gcp" ]; then
source ./secrets-export.sh
else
echo "Unrecognized OIDC_ENV $OIDC_ENV"
exit 1

39
pymongo/_gcp_helpers.py Normal file
View File

@ -0,0 +1,39 @@
# Copyright 2024-present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""GCP helpers."""
from __future__ import annotations
from typing import Any
from urllib.request import Request, urlopen
def _get_gcp_response(resource: str, timeout: float = 5) -> dict[str, Any]:
url = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity"
url += f"?audience={resource}"
headers = {"Metadata-Flavor": "Google", "Accept": "application/json"}
request = Request(url, headers=headers) # noqa: S310
try:
with urlopen(request, timeout=timeout) as response: # noqa: S310
status = response.status
body = response.read().decode("utf8")
except Exception as e:
msg = "Failed to acquire IMDS access token: %s" % e
raise ValueError(msg) from None
if status != 200:
msg = "Failed to acquire IMDS access token."
raise ValueError(msg)
return dict(access_token=body)

View File

@ -41,6 +41,7 @@ from pymongo.auth_oidc import (
_authenticate_oidc,
_get_authenticator,
_OIDCAzureCallback,
_OIDCGCPCallback,
_OIDCProperties,
_OIDCTestCallback,
)
@ -207,6 +208,13 @@ def _build_credentials_tuple(
"Azure environment for MONGODB-OIDC requires a TOKEN_RESOURCE auth mechanism property"
)
callback = _OIDCAzureCallback(token_resource)
elif environ == "gcp":
passwd = None
if not token_resource:
raise ConfigurationError(
"GCP provider for MONGODB-OIDC requires a TOKEN_RESOURCE auth mechanism property"
)
callback = _OIDCGCPCallback(token_resource)
else:
raise ConfigurationError(f"unrecognized ENVIRONMENT for MONGODB-OIDC: {environ}")
else:

View File

@ -26,6 +26,7 @@ import bson
from bson.binary import Binary
from pymongo._azure_helpers import _get_azure_response
from pymongo._csot import remaining
from pymongo._gcp_helpers import _get_gcp_response
from pymongo.errors import ConfigurationError, OperationFailure
if TYPE_CHECKING:
@ -133,6 +134,15 @@ class _OIDCAzureCallback(OIDCCallback):
)
class _OIDCGCPCallback(OIDCCallback):
def __init__(self, token_resource: str) -> None:
self.token_resource = token_resource
def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult:
resp = _get_gcp_response(self.token_resource, context.timeout_seconds)
return OIDCCallbackResult(access_token=resp["access_token"])
@dataclass
class _OIDCAuthenticator:
username: str

View File

@ -540,10 +540,37 @@
"credential": null
},
{
"description": "should throw and exception if no token audience is given for azure provider (MONGODB-OIDC)",
"description": "should throw an exception if no token audience is given for azure provider (MONGODB-OIDC)",
"uri": "mongodb://username@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure",
"valid": false,
"credential": null
},
{
"description": "should recognise the mechanism with gcp provider (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:foo",
"valid": true,
"credential": {
"username": null,
"password": null,
"source": "$external",
"mechanism": "MONGODB-OIDC",
"mechanism_properties": {
"ENVIRONMENT": "gcp",
"TOKEN_RESOURCE": "foo"
}
}
},
{
"description": "should throw an error for a username and password with gcp provider (MONGODB-OIDC)",
"uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:foo",
"valid": false,
"credential": null
},
{
"description": "should throw an error if not TOKEN_RESOURCE with gcp provider (MONGODB-OIDC)",
"uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp",
"valid": false,
"credential": null
}
]
}

View File

@ -27,12 +27,14 @@ from typing import Dict
sys.path[0:0] = [""]
import pprint
from test.unified_format import generate_test_classes
from test.utils import EventListener
from bson import SON
from pymongo import MongoClient
from pymongo._azure_helpers import _get_azure_response
from pymongo._gcp_helpers import _get_gcp_response
from pymongo.auth_oidc import (
OIDCCallback,
OIDCCallbackResult,
@ -75,10 +77,12 @@ class OIDCTestBase(unittest.TestCase):
return fid.read()
elif ENVIRON == "azure":
opts = parse_uri(self.uri_single)["options"]
resource = opts["authmechanismproperties"]["TOKEN_RESOURCE"]
return _get_azure_response(resource, username)["access_token"]
else:
raise RuntimeError(f"Invalid ENVIRONMENT {ENVIRON}")
token_aud = opts["authmechanismproperties"]["TOKEN_RESOURCE"]
return _get_azure_response(token_aud, username)["access_token"]
elif ENVIRON == "gcp":
opts = parse_uri(self.uri_single)["options"]
token_aud = opts["authmechanismproperties"]["TOKEN_RESOURCE"]
return _get_gcp_response(token_aud, username)["access_token"]
@contextmanager
def fail_point(self, command_args):

View File

@ -172,6 +172,11 @@ elif OIDC_ENV == "azure":
"ENVIRONMENT": "azure",
"TOKEN_RESOURCE": os.environ["AZUREOIDC_RESOURCE"],
}
elif OIDC_ENV == "gcp":
PLACEHOLDER_MAP["/uriOptions/authMechanismProperties"] = {
"ENVIRONMENT": "gcp",
"TOKEN_RESOURCE": os.environ["GCPOIDC_AUDIENCE"],
}
def interrupt_loop():