Skip to content

PYTHON-4845 Ensure ALLOWED_HOSTS is optional for Workload Usage #1998

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pymongo/asynchronous/auth_oidc.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def _get_authenticator(
properties = credentials.mechanism_properties

# Validate that the address is allowed.
if not properties.environment:
if properties.human_callback is not None:
found = False
allowed_hosts = properties.allowed_hosts
for patt in allowed_hosts:
Expand Down
8 changes: 5 additions & 3 deletions pymongo/auth_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ def _validate_canonicalize_host_name(value: str | bool) -> str | bool:
def _build_credentials_tuple(
mech: str,
source: Optional[str],
user: str,
passwd: str,
user: Optional[str],
passwd: Optional[str],
extra: Mapping[str, Any],
database: Optional[str],
) -> MongoCredential:
Expand Down Expand Up @@ -161,6 +161,8 @@ def _build_credentials_tuple(
"::1",
]
allowed_hosts = properties.get("ALLOWED_HOSTS", default_allowed)
if properties.get("ALLOWED_HOSTS", None) is not None and human_callback is None:
raise ConfigurationError("ALLOWED_HOSTS is only valid with OIDC_HUMAN_CALLBACK")
msg = (
"authentication with MONGODB-OIDC requires providing either a callback or a environment"
)
Expand Down Expand Up @@ -207,7 +209,7 @@ def _build_credentials_tuple(
environment=environ,
allowed_hosts=allowed_hosts,
token_resource=token_resource,
username=user,
username=user or "",
)
return MongoCredential(mech, "$external", user, passwd, oidc_props, _Cache())

Expand Down
6 changes: 4 additions & 2 deletions pymongo/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -873,8 +873,10 @@ def get_setter_key(x: str) -> str:
validator = _get_validator(opt, URI_OPTIONS_VALIDATOR_MAP, normed_key=normed_key)
validated = validator(opt, value)
except (ValueError, TypeError, ConfigurationError) as exc:
if normed_key == "authmechanismproperties" and any(
p in str(exc) for p in _MECH_PROP_MUST_RAISE
if (
normed_key == "authmechanismproperties"
and any(p in str(exc) for p in _MECH_PROP_MUST_RAISE)
and "is not a supported auth mechanism property" not in str(exc)
):
raise
if warn:
Expand Down
2 changes: 1 addition & 1 deletion pymongo/synchronous/auth_oidc.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def _get_authenticator(
properties = credentials.mechanism_properties

# Validate that the address is allowed.
if not properties.environment:
if properties.human_callback is not None:
found = False
allowed_hosts = properties.allowed_hosts
for patt in allowed_hosts:
Expand Down
38 changes: 33 additions & 5 deletions test/auth_oidc/test_auth_oidc.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,17 @@
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.auth_shared import _build_credentials_tuple
from pymongo.cursor_shared import CursorType
from pymongo.errors import AutoReconnect, ConfigurationError, OperationFailure
from pymongo.hello import HelloCompat
from pymongo.operations import InsertOne
from pymongo.synchronous.auth_oidc import OIDCCallback, OIDCCallbackContext, OIDCCallbackResult
from pymongo.synchronous.auth_oidc import (
OIDCCallback,
OIDCCallbackContext,
OIDCCallbackResult,
_get_authenticator,
)
from pymongo.uri_parser import parse_uri

ROOT = Path(__file__).parent.parent.resolve()
Expand Down Expand Up @@ -103,7 +109,6 @@ def fail_point(self, command_args):
client.close()


@pytest.mark.auth_oidc
class TestAuthOIDCHuman(OIDCTestBase):
uri: str

Expand Down Expand Up @@ -838,12 +843,35 @@ def test_2_4_invalid_client_configuration_with_callback(self):
self.create_client(authmechanismproperties=props)

def test_2_5_invalid_use_of_ALLOWED_HOSTS(self):
# Create an OIDC configured client with auth mechanism properties `{"ENVIRONMENT": "azure", "ALLOWED_HOSTS": []}`.
props: Dict = {"ENVIRONMENT": "azure", "ALLOWED_HOSTS": []}
# Create an OIDC configured client with auth mechanism properties `{"ENVIRONMENT": "test", "ALLOWED_HOSTS": []}`.
props: Dict = {"ENVIRONMENT": "test", "ALLOWED_HOSTS": []}
# Assert it returns a client configuration error.
with self.assertRaises(ConfigurationError):
self.create_client(authmechanismproperties=props)

# Create an OIDC configured client with auth mechanism properties `{"OIDC_CALLBACK": "<my_callback>", "ALLOWED_HOSTS": []}`.
props: Dict = {"OIDC_CALLBACK": self.create_request_cb(), "ALLOWED_HOSTS": []}
# Assert it returns a client configuration error.
with self.assertRaises(ConfigurationError):
self.create_client(authmechanismproperties=props)

def test_2_6_ALLOWED_HOSTS_defaults_ignored(self):
# Create a MongoCredential for OIDC with a machine callback.
props = {"OIDC_CALLBACK": self.create_request_cb()}
extra = dict(authmechanismproperties=props)
mongo_creds = _build_credentials_tuple("MONGODB-OIDC", None, "foo", None, extra, "test")
# Assert that creating an authenticator for example.com does not result in an error.
authenticator = _get_authenticator(mongo_creds, ("example.com", 30))
assert authenticator.properties.username == "foo"

# Create a MongoCredential for OIDC with an ENVIRONMENT.
props = {"ENVIRONMENT": "test"}
extra = dict(authmechanismproperties=props)
mongo_creds = _build_credentials_tuple("MONGODB-OIDC", None, None, None, extra, "test")
# Assert that creating an authenticator for example.com does not result in an error.
authenticator = _get_authenticator(mongo_creds, ("example.com", 30))
assert authenticator.properties.username == ""

def test_3_1_authentication_failure_with_cached_tokens_fetch_a_new_token_and_retry(self):
# Create a MongoClient and an OIDC callback that implements the provider logic.
client = self.create_client()
Expand Down Expand Up @@ -909,7 +937,7 @@ def test_3_3_unexpected_error_code_does_not_clear_cache(self):
# Assert that the callback has been called once.
self.assertEqual(self.request_called, 1)

def test_4_1_reauthentication_succeds(self):
def test_4_1_reauthentication_succeeds(self):
# Create a ``MongoClient`` configured with a custom OIDC callback that
# implements the provider logic.
client = self.create_client()
Expand Down
Loading