PYTHON-4330 Add Kubernetes Support for OIDC (#1759)

This commit is contained in:
Steven Silvester 2024-11-04 10:26:07 -06:00 committed by GitHub
parent a9caaf0d6a
commit 57fd616ace
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 190 additions and 85 deletions

View File

@ -520,6 +520,18 @@ functions:
args:
- .evergreen/run-mongodb-oidc-test.sh
"run oidc k8s auth test":
- command: subprocess.exec
type: test
params:
binary: bash
working_dir: src
env:
OIDC_ENV: k8s
include_expansions_in_env: ["DRIVERS_TOOLS", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "K8S_VARIANT"]
args:
- ${PROJECT_DIRECTORY}/.evergreen/run-mongodb-oidc-remote-test.sh
"run aws auth test with aws credentials as environment variables":
- command: shell.exec
type: test
@ -873,6 +885,32 @@ task_groups:
tasks:
- oidc-auth-test-gcp
- name: testk8soidc_task_group
setup_group:
- func: fetch source
- func: prepare resources
- func: fix absolute paths
- func: make files executable
- command: ec2.assume_role
params:
role_arn: ${aws_test_secrets_role}
duration_seconds: 1800
- command: subprocess.exec
params:
binary: bash
args:
- ${DRIVERS_TOOLS}/.evergreen/auth_oidc/k8s/setup.sh
teardown_task:
- command: subprocess.exec
params:
binary: bash
args:
- ${DRIVERS_TOOLS}/.evergreen/auth_oidc/k8s/teardown.sh
setup_group_can_fail_task: true
setup_group_timeout_secs: 1800
tasks:
- oidc-auth-test-k8s
- name: testoidc_task_group
setup_group:
- func: fetch source
@ -1548,40 +1586,41 @@ tasks:
- name: "oidc-auth-test-azure"
commands:
- command: shell.exec
- command: subprocess.exec
type: test
params:
shell: bash
script: |-
set -o errexit
. src/.evergreen/scripts/env.sh
cd src
git add .
git commit -m "add files"
export AZUREOIDC_DRIVERS_TAR_FILE=/tmp/mongo-python-driver.tgz
git archive -o $AZUREOIDC_DRIVERS_TAR_FILE HEAD
export AZUREOIDC_TEST_CMD="OIDC_ENV=azure ./.evergreen/run-mongodb-oidc-test.sh"
bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh
binary: bash
working_dir: src
env:
OIDC_ENV: azure
include_expansions_in_env: ["DRIVERS_TOOLS"]
args:
- ${PROJECT_DIRECTORY}/.evergreen/run-mongodb-oidc-remote-test.sh
- name: "oidc-auth-test-gcp"
commands:
- command: shell.exec
- command: subprocess.exec
type: test
params:
shell: bash
script: |-
set -o errexit
. src/.evergreen/scripts/env.sh
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
binary: bash
working_dir: src
env:
OIDC_ENV: gcp
include_expansions_in_env: ["DRIVERS_TOOLS"]
args:
- ${PROJECT_DIRECTORY}/.evergreen/run-mongodb-oidc-remote-test.sh
- name: "oidc-auth-test-k8s"
commands:
- func: "run oidc k8s auth test"
vars:
K8S_VARIANT: eks
- func: "run oidc k8s auth test"
vars:
K8S_VARIANT: gke
- func: "run oidc k8s auth test"
vars:
K8S_VARIANT: aks
# }}}
- name: "coverage-report"
tags: ["coverage"]
@ -1740,20 +1779,6 @@ buildvariants:
tasks:
- name: "coverage-report"
- name: testazureoidc-variant
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
- name: testgcpkms-variant
display_name: "GCP KMS"
run_on:

View File

@ -955,12 +955,15 @@ buildvariants:
VERSION: "8.0"
# Oidc auth tests
- name: oidc-auth-rhel8
- name: oidc-auth-ubuntu-22
tasks:
- name: testoidc_task_group
display_name: OIDC Auth RHEL8
- name: testazureoidc_task_group
- name: testgcpoidc_task_group
- name: testk8soidc_task_group
display_name: OIDC Auth Ubuntu-22
run_on:
- rhel87-small
- ubuntu2204-small
batchtime: 20160
- name: oidc-auth-macos
tasks:

View File

@ -0,0 +1,60 @@
#!/bin/bash
set +x # Disable debug trace
set -eu
echo "Running MONGODB-OIDC remote tests"
OIDC_ENV=${OIDC_ENV:-"test"}
# Make sure DRIVERS_TOOLS is set.
if [ -z "$DRIVERS_TOOLS" ]; then
echo "Must specify DRIVERS_TOOLS"
exit 1
fi
# Set up the remote files to test.
git add .
git commit -m "add files" || true
export TEST_TAR_FILE=/tmp/mongo-python-driver.tgz
git archive -o $TEST_TAR_FILE HEAD
pushd $DRIVERS_TOOLS
if [ $OIDC_ENV == "test" ]; then
echo "Test OIDC environment does not support remote test!"
exit 1
elif [ $OIDC_ENV == "azure" ]; then
export AZUREOIDC_DRIVERS_TAR_FILE=$TEST_TAR_FILE
export AZUREOIDC_TEST_CMD="OIDC_ENV=azure ./.evergreen/run-mongodb-oidc-test.sh"
bash ./.evergreen/auth_oidc/azure/run-driver-test.sh
elif [ $OIDC_ENV == "gcp" ]; then
export GCPOIDC_DRIVERS_TAR_FILE=$TEST_TAR_FILE
export GCPOIDC_TEST_CMD="OIDC_ENV=gcp ./.evergreen/run-mongodb-oidc-test.sh"
bash ./.evergreen/auth_oidc/gcp/run-driver-test.sh
elif [ $OIDC_ENV == "k8s" ]; then
# Make sure K8S_VARIANT is set.
if [ -z "$K8S_VARIANT" ]; then
echo "Must specify K8S_VARIANT"
popd
exit 1
fi
bash ./.evergreen/auth_oidc/k8s/setup-pod.sh
bash ./.evergreen/auth_oidc/k8s/run-self-test.sh
export K8S_DRIVERS_TAR_FILE=$TEST_TAR_FILE
export K8S_TEST_CMD="OIDC_ENV=k8s ./.evergreen/run-mongodb-oidc-test.sh"
source ./.evergreen/auth_oidc/k8s/secrets-export.sh # for MONGODB_URI
bash ./.evergreen/auth_oidc/k8s/run-driver-test.sh
bash ./.evergreen/auth_oidc/k8s/teardown-pod.sh
else
echo "Unrecognized OIDC_ENV $OIDC_ENV"
pod
exit 1
fi
popd

View File

@ -21,6 +21,9 @@ elif [ $OIDC_ENV == "azure" ]; then
elif [ $OIDC_ENV == "gcp" ]; then
source ./secrets-export.sh
elif [ $OIDC_ENV == "k8s" ]; then
echo "Running oidc on k8s"
else
echo "Unrecognized OIDC_ENV $OIDC_ENV"
exit 1

View File

@ -615,10 +615,14 @@ def create_serverless_variants():
def create_oidc_auth_variants():
variants = []
for host in ["rhel8", "macos", "win64"]:
other_tasks = ["testazureoidc_task_group", "testgcpoidc_task_group", "testk8soidc_task_group"]
for host in ["ubuntu22", "macos", "win64"]:
tasks = ["testoidc_task_group"]
if host == "ubuntu22":
tasks += other_tasks
variants.append(
create_variant(
["testoidc_task_group"],
tasks,
get_display_name("OIDC Auth", host),
host=host,
batchtime=BATCHTIME_WEEK * 2,

View File

@ -116,3 +116,17 @@ class _OIDCGCPCallback(OIDCCallback):
def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult:
resp = _get_gcp_response(self.token_resource, context.timeout_seconds)
return OIDCCallbackResult(access_token=resp["access_token"])
class _OIDCK8SCallback(OIDCCallback):
def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult:
return OIDCCallbackResult(access_token=_get_k8s_token())
def _get_k8s_token() -> str:
fname = "/var/run/secrets/kubernetes.io/serviceaccount/token"
for key in ["AZURE_FEDERATED_TOKEN_FILE", "AWS_WEB_IDENTITY_TOKEN_FILE"]:
if key in os.environ:
fname = os.environ[key]
with open(fname) as fid:
return fid.read()

View File

@ -26,6 +26,7 @@ from bson import Binary
from pymongo.auth_oidc_shared import (
_OIDCAzureCallback,
_OIDCGCPCallback,
_OIDCK8SCallback,
_OIDCProperties,
_OIDCTestCallback,
)
@ -192,6 +193,9 @@ def _build_credentials_tuple(
"GCP provider for MONGODB-OIDC requires a TOKEN_RESOURCE auth mechanism property"
)
callback = _OIDCGCPCallback(token_resource)
elif environ == "k8s":
passwd = None
callback = _OIDCK8SCallback()
else:
raise ConfigurationError(f"unrecognized ENVIRONMENT for MONGODB-OIDC: {environ}")
else:

View File

@ -626,6 +626,26 @@
"uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp",
"valid": false,
"credential": null
},
{
"description": "should recognise the mechanism with k8s provider (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:k8s",
"valid": true,
"credential": {
"username": null,
"password": null,
"source": "$external",
"mechanism": "MONGODB-OIDC",
"mechanism_properties": {
"ENVIRONMENT": "k8s"
}
}
},
{
"description": "should throw an error for a username and password with k8s provider (MONGODB-OIDC)",
"uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:k8s",
"valid": false,
"credential": null
}
]
}

View File

@ -37,6 +37,7 @@ 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_shared import _get_k8s_token
from pymongo.cursor_shared import CursorType
from pymongo.errors import AutoReconnect, ConfigurationError, OperationFailure
from pymongo.hello import HelloCompat
@ -84,6 +85,10 @@ class OIDCTestBase(PyMongoTestCase):
opts = parse_uri(self.uri_single)["options"]
token_aud = opts["authmechanismproperties"]["TOKEN_RESOURCE"]
return _get_gcp_response(token_aud, username)["access_token"]
elif ENVIRON == "k8s":
return _get_k8s_token()
else:
raise ValueError(f"Unknown ENVIRON: {ENVIRON}")
@contextmanager
def fail_point(self, command_args):
@ -758,7 +763,9 @@ class TestAuthOIDCMachine(OIDCTestBase):
kwargs["retryReads"] = False
if not len(args):
args = [self.uri_single]
return MongoClient(*args, authmechanismproperties=props, **kwargs)
client = MongoClient(*args, authmechanismproperties=props, **kwargs)
self.addCleanup(client.close)
return client
def test_1_1_callback_is_called_during_reauthentication(self):
# Create a ``MongoClient`` configured with a custom OIDC callback that
@ -768,8 +775,6 @@ class TestAuthOIDCMachine(OIDCTestBase):
client.test.test.find_one()
# Assert that the callback was called 1 time.
self.assertEqual(self.request_called, 1)
# Close the client.
client.close()
def test_1_2_callback_is_called_once_for_multiple_connections(self):
# Create a ``MongoClient`` configured with a custom OIDC callback that
@ -790,8 +795,6 @@ class TestAuthOIDCMachine(OIDCTestBase):
thread.join()
# Assert that the callback was called 1 time.
self.assertEqual(self.request_called, 1)
# Close the client.
client.close()
def test_2_1_valid_callback_inputs(self):
# Create a MongoClient configured with an OIDC callback that validates its inputs and returns a valid access token.
@ -800,8 +803,6 @@ class TestAuthOIDCMachine(OIDCTestBase):
client.test.test.find_one()
# Assert that the OIDC callback was called with the appropriate inputs, including the timeout parameter if possible. Ensure that there are no unexpected fields.
self.assertEqual(self.request_called, 1)
# Close the client.
client.close()
def test_2_2_oidc_callback_returns_null(self):
# Create a MongoClient configured with an OIDC callback that returns null.
@ -813,8 +814,6 @@ class TestAuthOIDCMachine(OIDCTestBase):
# Perform a find operation that fails.
with self.assertRaises(ValueError):
client.test.test.find_one()
# Close the client.
client.close()
def test_2_3_oidc_callback_returns_missing_data(self):
# Create a MongoClient configured with an OIDC callback that returns data not conforming to the OIDCCredential with missing fields.
@ -829,8 +828,6 @@ class TestAuthOIDCMachine(OIDCTestBase):
# Perform a find operation that fails.
with self.assertRaises(ValueError):
client.test.test.find_one()
# Close the client.
client.close()
def test_2_4_invalid_client_configuration_with_callback(self):
# Create a MongoClient configured with an OIDC callback and auth mechanism property ENVIRONMENT:test.
@ -870,8 +867,6 @@ class TestAuthOIDCMachine(OIDCTestBase):
client.test.test.find_one()
# Verify that the callback was called 1 time.
self.assertEqual(self.request_called, 1)
# Close the client.
client.close()
def test_3_2_authentication_failures_without_cached_tokens_returns_an_error(self):
# Create a MongoClient configured with retryReads=false and an OIDC callback that always returns invalid access tokens.
@ -889,8 +884,6 @@ class TestAuthOIDCMachine(OIDCTestBase):
client.test.test.find_one()
# Verify that the callback was called 1 time.
self.assertEqual(callback.count, 1)
# Close the client.
client.close()
def test_3_3_unexpected_error_code_does_not_clear_cache(self):
# Create a ``MongoClient`` with a human callback that returns a valid token
@ -916,9 +909,6 @@ class TestAuthOIDCMachine(OIDCTestBase):
# Assert that the callback has been called once.
self.assertEqual(self.request_called, 1)
# Close the client.
client.close()
def test_4_1_reauthentication_succeds(self):
# Create a ``MongoClient`` configured with a custom OIDC callback that
# implements the provider logic.
@ -938,9 +928,6 @@ class TestAuthOIDCMachine(OIDCTestBase):
# handshake, and again during reauthentication).
self.assertEqual(self.request_called, 2)
# Close the client.
client.close()
def test_4_2_read_commands_fail_if_reauthentication_fails(self):
# Create a ``MongoClient`` whose OIDC callback returns one good token and then
# bad tokens after the first call.
@ -977,9 +964,6 @@ class TestAuthOIDCMachine(OIDCTestBase):
# Verify that the callback was called 2 times.
self.assertEqual(callback.count, 2)
# Close the client.
client.close()
def test_4_3_write_commands_fail_if_reauthentication_fails(self):
# Create a ``MongoClient`` whose OIDC callback returns one good token and then
# bad token after the first call.
@ -1016,12 +1000,9 @@ class TestAuthOIDCMachine(OIDCTestBase):
# Verify that the callback was called 2 times.
self.assertEqual(callback.count, 2)
# Close the client.
client.close()
def test_4_4_speculative_authentication_should_be_ignored_on_reauthentication(self):
# Create an OIDC configured client that can listen for `SaslStart` commands.
listener = OvertCommandListener()
listener = EventListener()
client = self.create_client(event_listeners=[listener])
# Preload the *Client Cache* with a valid access token to enforce Speculative Authentication.
@ -1061,9 +1042,6 @@ class TestAuthOIDCMachine(OIDCTestBase):
# Assert there were `SaslStart` commands executed.
assert any(event.command_name.lower() == "saslstart" for event in listener.started_events)
# Close the client.
client.close()
def test_5_1_azure_with_no_username(self):
if ENVIRON != "azure":
raise unittest.SkipTest("Test is only supported on Azure")
@ -1073,7 +1051,6 @@ class TestAuthOIDCMachine(OIDCTestBase):
props = dict(TOKEN_RESOURCE=resource, ENVIRONMENT="azure")
client = self.create_client(authMechanismProperties=props)
client.test.test.find_one()
client.close()
def test_5_2_azure_with_bad_username(self):
if ENVIRON != "azure":
@ -1086,7 +1063,6 @@ class TestAuthOIDCMachine(OIDCTestBase):
client = self.create_client(username="bad", authmechanismproperties=props)
with self.assertRaises(ValueError):
client.test.test.find_one()
client.close()
def test_speculative_auth_success(self):
client1 = self.create_client()
@ -1108,10 +1084,6 @@ class TestAuthOIDCMachine(OIDCTestBase):
# Perform a find operation.
client2.test.test.find_one()
# Close the clients.
client2.close()
client1.close()
def test_reauthentication_succeeds_multiple_connections(self):
client1 = self.create_client()
client2 = self.create_client()
@ -1151,8 +1123,6 @@ class TestAuthOIDCMachine(OIDCTestBase):
client2.test.test.find_one()
self.assertEqual(self.request_called, 3)
client1.close()
client2.close()
if __name__ == "__main__":

View File

@ -137,6 +137,8 @@ elif OIDC_ENV == "gcp":
"ENVIRONMENT": "gcp",
"TOKEN_RESOURCE": os.environ["GCPOIDC_AUDIENCE"],
}
elif OIDC_ENV == "k8s":
PLACEHOLDER_MAP["/uriOptions/authMechanismProperties"] = {"ENVIRONMENT": "k8s"}
def with_metaclass(meta, *bases):