PYTHON-3664 OIDC: Automatic token acquisition for GCP Identity Provider (#1540)
This commit is contained in:
parent
c154c6b67b
commit
1e0ef67ab8
@ -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]
|
||||
|
||||
@ -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
39
pymongo/_gcp_helpers.py
Normal 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)
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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():
|
||||
|
||||
Loading…
Reference in New Issue
Block a user