PYTHON-4256 OIDC Spec Cleanup (#1556)
This commit is contained in:
parent
8be31bf8f1
commit
efe8cc38a6
@ -28,6 +28,7 @@ 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
|
||||
from pymongo.helpers import _AUTHENTICATION_FAILURE_CODE
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pymongo.auth import MongoCredential
|
||||
@ -37,7 +38,7 @@ if TYPE_CHECKING:
|
||||
@dataclass
|
||||
class OIDCIdPInfo:
|
||||
issuer: str
|
||||
clientId: str
|
||||
clientId: Optional[str] = field(default=None)
|
||||
requestScopes: Optional[list[str]] = field(default=None)
|
||||
|
||||
|
||||
@ -189,30 +190,43 @@ class _OIDCAuthenticator:
|
||||
|
||||
def _authenticate_machine(self, conn: Connection) -> Mapping[str, Any]:
|
||||
# If there is a cached access token, try to authenticate with it. If
|
||||
# authentication fails, it's possible the cached access token is expired. In
|
||||
# that case, invalidate the access token, fetch a new access token, and try
|
||||
# to authenticate again.
|
||||
# authentication fails with error code 18, invalidate the access token,
|
||||
# fetch a new access token, and try to authenticate again. If authentication
|
||||
# fails for any other reason, raise the error to the user.
|
||||
if self.access_token:
|
||||
try:
|
||||
return self._sasl_start_jwt(conn)
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
except OperationFailure as e:
|
||||
if self._is_auth_error(e):
|
||||
return self._authenticate_machine(conn)
|
||||
raise
|
||||
return self._sasl_start_jwt(conn)
|
||||
|
||||
def _authenticate_human(self, conn: Connection) -> Optional[Mapping[str, Any]]:
|
||||
# If we have a cached access token, try a JwtStepRequest.
|
||||
# authentication fails with error code 18, invalidate the access token,
|
||||
# and try to authenticate again. If authentication fails for any other
|
||||
# reason, raise the error to the user.
|
||||
if self.access_token:
|
||||
try:
|
||||
return self._sasl_start_jwt(conn)
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
except OperationFailure as e:
|
||||
if self._is_auth_error(e):
|
||||
return self._authenticate_human(conn)
|
||||
raise
|
||||
|
||||
# If we have a cached refresh token, try a JwtStepRequest with that.
|
||||
# If authentication fails with error code 18, invalidate the access and
|
||||
# refresh tokens, and try to authenticate again. If authentication fails for
|
||||
# any other reason, raise the error to the user.
|
||||
if self.refresh_token:
|
||||
try:
|
||||
return self._sasl_start_jwt(conn)
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
except OperationFailure as e:
|
||||
if self._is_auth_error(e):
|
||||
self.refresh_token = None
|
||||
return self._authenticate_human(conn)
|
||||
raise
|
||||
|
||||
# Start a new Two-Step SASL conversation.
|
||||
# Run a PrincipalStepRequest to get the IdpInfo.
|
||||
@ -280,10 +294,16 @@ class _OIDCAuthenticator:
|
||||
def _run_command(self, conn: Connection, cmd: MutableMapping[str, Any]) -> Mapping[str, Any]:
|
||||
try:
|
||||
return conn.command("$external", cmd, no_reauth=True) # type: ignore[call-arg]
|
||||
except OperationFailure:
|
||||
self._invalidate(conn)
|
||||
except OperationFailure as e:
|
||||
if self._is_auth_error(e):
|
||||
self._invalidate(conn)
|
||||
raise
|
||||
|
||||
def _is_auth_error(self, err: Exception) -> bool:
|
||||
if not isinstance(err, OperationFailure):
|
||||
return False
|
||||
return err.code == _AUTHENTICATION_FAILURE_CODE
|
||||
|
||||
def _invalidate(self, conn: Connection) -> None:
|
||||
# Ignore the invalidation if a token gen id is given and is less than our
|
||||
# current token gen id.
|
||||
|
||||
@ -426,7 +426,6 @@ _MECHANISM_PROPS = frozenset(
|
||||
"AWS_SESSION_TOKEN",
|
||||
"ENVIRONMENT",
|
||||
"TOKEN_RESOURCE",
|
||||
"ALLOWED_HOSTS",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@ -90,6 +90,9 @@ _RETRYABLE_ERROR_CODES: frozenset = _NOT_PRIMARY_CODES | frozenset(
|
||||
# Server code raised when re-authentication is required
|
||||
_REAUTHENTICATION_REQUIRED_CODE: int = 391
|
||||
|
||||
# Server code raised when authentication fails.
|
||||
_AUTHENTICATION_FAILURE_CODE: int = 18
|
||||
|
||||
|
||||
def _gen_index_name(keys: _IndexList) -> str:
|
||||
"""Generate an index name from the set of fields it is over."""
|
||||
|
||||
@ -497,6 +497,12 @@
|
||||
"valid": false,
|
||||
"credential": null
|
||||
},
|
||||
{
|
||||
"description": "should throw an exception custom callback is chosen but no callback is provided (MONGODB-OIDC)",
|
||||
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:custom",
|
||||
"valid": false,
|
||||
"credential": null
|
||||
},
|
||||
{
|
||||
"description": "should throw an exception if neither provider nor callbacks specified (MONGODB-OIDC)",
|
||||
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC",
|
||||
@ -573,4 +579,4 @@
|
||||
"credential": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,191 +0,0 @@
|
||||
{
|
||||
"description": "reauthenticate_with_retry",
|
||||
"schemaVersion": "1.12",
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "6.3",
|
||||
"auth": true
|
||||
}
|
||||
],
|
||||
"createEntities": [
|
||||
{
|
||||
"client": {
|
||||
"id": "client0",
|
||||
"uriOptions": {
|
||||
"retryReads": true,
|
||||
"retryWrites": true
|
||||
},
|
||||
"observeEvents": [
|
||||
"commandStartedEvent",
|
||||
"commandSucceededEvent",
|
||||
"commandFailedEvent"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"database": {
|
||||
"id": "database0",
|
||||
"client": "client0",
|
||||
"databaseName": "db"
|
||||
}
|
||||
},
|
||||
{
|
||||
"collection": {
|
||||
"id": "collection0",
|
||||
"database": "database0",
|
||||
"collectionName": "collName"
|
||||
}
|
||||
}
|
||||
],
|
||||
"initialData": [
|
||||
{
|
||||
"collectionName": "collName",
|
||||
"databaseName": "db",
|
||||
"documents": []
|
||||
}
|
||||
],
|
||||
"tests": [
|
||||
{
|
||||
"description": "Read command should reauthenticate when receive ReauthenticationRequired error code and retryReads=true",
|
||||
"operations": [
|
||||
{
|
||||
"name": "failPoint",
|
||||
"object": "testRunner",
|
||||
"arguments": {
|
||||
"client": "client0",
|
||||
"failPoint": {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": {
|
||||
"times": 1
|
||||
},
|
||||
"data": {
|
||||
"failCommands": [
|
||||
"find"
|
||||
],
|
||||
"errorCode": 391
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "find",
|
||||
"arguments": {
|
||||
"filter": {}
|
||||
},
|
||||
"object": "collection0",
|
||||
"expectResult": []
|
||||
}
|
||||
],
|
||||
"expectEvents": [
|
||||
{
|
||||
"client": "client0",
|
||||
"events": [
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"find": "collName",
|
||||
"filter": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandFailedEvent": {
|
||||
"commandName": "find"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"find": "collName",
|
||||
"filter": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandSucceededEvent": {
|
||||
"commandName": "find"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Write command should reauthenticate when receive ReauthenticationRequired error code and retryWrites=true",
|
||||
"operations": [
|
||||
{
|
||||
"name": "failPoint",
|
||||
"object": "testRunner",
|
||||
"arguments": {
|
||||
"client": "client0",
|
||||
"failPoint": {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": {
|
||||
"times": 1
|
||||
},
|
||||
"data": {
|
||||
"failCommands": [
|
||||
"insert"
|
||||
],
|
||||
"errorCode": 391
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "insertOne",
|
||||
"object": "collection0",
|
||||
"arguments": {
|
||||
"document": {
|
||||
"_id": 1,
|
||||
"x": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"expectEvents": [
|
||||
{
|
||||
"client": "client0",
|
||||
"events": [
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"insert": "collName",
|
||||
"documents": [
|
||||
{
|
||||
"_id": 1,
|
||||
"x": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandFailedEvent": {
|
||||
"commandName": "insert"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"insert": "collName",
|
||||
"documents": [
|
||||
{
|
||||
"_id": 1,
|
||||
"x": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandSucceededEvent": {
|
||||
"commandName": "insert"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,191 +0,0 @@
|
||||
{
|
||||
"description": "reauthenticate_without_retry",
|
||||
"schemaVersion": "1.12",
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "6.3",
|
||||
"auth": true
|
||||
}
|
||||
],
|
||||
"createEntities": [
|
||||
{
|
||||
"client": {
|
||||
"id": "client0",
|
||||
"uriOptions": {
|
||||
"retryReads": false,
|
||||
"retryWrites": false
|
||||
},
|
||||
"observeEvents": [
|
||||
"commandStartedEvent",
|
||||
"commandSucceededEvent",
|
||||
"commandFailedEvent"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"database": {
|
||||
"id": "database0",
|
||||
"client": "client0",
|
||||
"databaseName": "db"
|
||||
}
|
||||
},
|
||||
{
|
||||
"collection": {
|
||||
"id": "collection0",
|
||||
"database": "database0",
|
||||
"collectionName": "collName"
|
||||
}
|
||||
}
|
||||
],
|
||||
"initialData": [
|
||||
{
|
||||
"collectionName": "collName",
|
||||
"databaseName": "db",
|
||||
"documents": []
|
||||
}
|
||||
],
|
||||
"tests": [
|
||||
{
|
||||
"description": "Read command should reauthenticate when receive ReauthenticationRequired error code and retryReads=false",
|
||||
"operations": [
|
||||
{
|
||||
"name": "failPoint",
|
||||
"object": "testRunner",
|
||||
"arguments": {
|
||||
"client": "client0",
|
||||
"failPoint": {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": {
|
||||
"times": 1
|
||||
},
|
||||
"data": {
|
||||
"failCommands": [
|
||||
"find"
|
||||
],
|
||||
"errorCode": 391
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "find",
|
||||
"arguments": {
|
||||
"filter": {}
|
||||
},
|
||||
"object": "collection0",
|
||||
"expectResult": []
|
||||
}
|
||||
],
|
||||
"expectEvents": [
|
||||
{
|
||||
"client": "client0",
|
||||
"events": [
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"find": "collName",
|
||||
"filter": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandFailedEvent": {
|
||||
"commandName": "find"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"find": "collName",
|
||||
"filter": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandSucceededEvent": {
|
||||
"commandName": "find"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Write command should reauthenticate when receive ReauthenticationRequired error code and retryWrites=false",
|
||||
"operations": [
|
||||
{
|
||||
"name": "failPoint",
|
||||
"object": "testRunner",
|
||||
"arguments": {
|
||||
"client": "client0",
|
||||
"failPoint": {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": {
|
||||
"times": 1
|
||||
},
|
||||
"data": {
|
||||
"failCommands": [
|
||||
"insert"
|
||||
],
|
||||
"errorCode": 391
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "insertOne",
|
||||
"object": "collection0",
|
||||
"arguments": {
|
||||
"document": {
|
||||
"_id": 1,
|
||||
"x": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"expectEvents": [
|
||||
{
|
||||
"client": "client0",
|
||||
"events": [
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"insert": "collName",
|
||||
"documents": [
|
||||
{
|
||||
"_id": 1,
|
||||
"x": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandFailedEvent": {
|
||||
"commandName": "insert"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"insert": "collName",
|
||||
"documents": [
|
||||
{
|
||||
"_id": 1,
|
||||
"x": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandSucceededEvent": {
|
||||
"commandName": "insert"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -35,10 +35,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 import (
|
||||
OIDCCallback,
|
||||
OIDCCallbackResult,
|
||||
)
|
||||
from pymongo.auth_oidc import OIDCCallback, OIDCCallbackContext, OIDCCallbackResult
|
||||
from pymongo.cursor import CursorType
|
||||
from pymongo.errors import AutoReconnect, ConfigurationError, OperationFailure
|
||||
from pymongo.hello import HelloCompat
|
||||
@ -107,15 +104,24 @@ class TestAuthOIDCHuman(OIDCTestBase):
|
||||
raise ValueError("Missing OIDC_DOMAIN")
|
||||
super().setUpClass()
|
||||
|
||||
def setUp(self):
|
||||
self.refresh_present = 0
|
||||
super().setUp()
|
||||
|
||||
def create_request_cb(self, username="test_user1", sleep=0):
|
||||
def request_token(context):
|
||||
def request_token(context: OIDCCallbackContext):
|
||||
# Validate the info.
|
||||
self.assertIsInstance(context.idp_info.issuer, str)
|
||||
self.assertIsInstance(context.idp_info.clientId, str)
|
||||
if context.idp_info.clientId is not None:
|
||||
self.assertIsInstance(context.idp_info.clientId, str)
|
||||
|
||||
# Validate the timeout.
|
||||
timeout_seconds = context.timeout_seconds
|
||||
self.assertEqual(timeout_seconds, 60 * 5)
|
||||
|
||||
if context.refresh_token:
|
||||
self.refresh_present += 1
|
||||
|
||||
token = self.get_token(username)
|
||||
resp = OIDCCallbackResult(access_token=token, refresh_token=token)
|
||||
|
||||
@ -131,7 +137,7 @@ class TestAuthOIDCHuman(OIDCTestBase):
|
||||
|
||||
def create_client(self, *args, **kwargs):
|
||||
username = kwargs.get("username", "test_user1")
|
||||
if kwargs.get("username"):
|
||||
if kwargs.get("username") in ["test_user1", "test_user2"]:
|
||||
kwargs["username"] = f"{username}@{DOMAIN}"
|
||||
request_cb = kwargs.pop("request_cb", self.create_request_cb(username=username))
|
||||
props = kwargs.pop("authmechanismproperties", {"OIDC_HUMAN_CALLBACK": request_cb})
|
||||
@ -219,6 +225,26 @@ class TestAuthOIDCHuman(OIDCTestBase):
|
||||
# Close the client.
|
||||
client.close()
|
||||
|
||||
def test_1_7_allowed_hosts_in_connection_string_ignored(self):
|
||||
# Create an OIDC configured client with the connection string: `mongodb+srv://example.com/?authMechanism=MONGODB-OIDC&authMechanismProperties=ALLOWED_HOSTS:%5B%22example.com%22%5D` and a Human Callback.
|
||||
# Assert that the creation of the client raises a configuration error.
|
||||
uri = "mongodb+srv://example.com?authMechanism=MONGODB-OIDC&authMechanismProperties=ALLOWED_HOSTS:%5B%22example.com%22%5D"
|
||||
with self.assertRaises(ConfigurationError), warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
_ = MongoClient(
|
||||
uri, authmechanismproperties=dict(OIDC_HUMAN_CALLBACK=self.create_request_cb())
|
||||
)
|
||||
|
||||
def test_1_8_machine_idp_human_callback(self):
|
||||
if not os.environ.get("OIDC_IS_LOCAL"):
|
||||
raise unittest.SkipTest("Test Requires Local OIDC server")
|
||||
# Create a client with MONGODB_URI_SINGLE, a username of test_machine, authMechanism=MONGODB-OIDC, and the OIDC human callback.
|
||||
client = self.create_client(username="test_machine")
|
||||
# Perform a find operation that succeeds.
|
||||
client.test.test.find_one()
|
||||
# Close the client.
|
||||
client.close()
|
||||
|
||||
def test_2_1_valid_callback_inputs(self):
|
||||
# Create a MongoClient with a human callback that validates its inputs and returns a valid access token.
|
||||
client = self.create_client()
|
||||
@ -228,7 +254,7 @@ class TestAuthOIDCHuman(OIDCTestBase):
|
||||
# Close the client.
|
||||
client.close()
|
||||
|
||||
def test_2_2_OIDC_HUMAN_CALLBACK_returns_missing_data(self):
|
||||
def test_2_2_callback_returns_missing_data(self):
|
||||
# Create a MongoClient with a human callback that returns data not conforming to the OIDCCredential with missing fields.
|
||||
class CustomCB(OIDCCallback):
|
||||
def fetch(self, ctx):
|
||||
@ -241,6 +267,29 @@ class TestAuthOIDCHuman(OIDCTestBase):
|
||||
# Close the client.
|
||||
client.close()
|
||||
|
||||
def test_2_3_refresh_token_is_passed_to_the_callback(self):
|
||||
# Create a MongoClient with a human callback that checks for the presence of a refresh token.
|
||||
client = self.create_client()
|
||||
|
||||
# Perform a find operation that succeeds.
|
||||
client.test.test.find_one()
|
||||
|
||||
# Set a fail point for ``find`` commands.
|
||||
with self.fail_point(
|
||||
{
|
||||
"mode": {"times": 1},
|
||||
"data": {"failCommands": ["find"], "errorCode": 391},
|
||||
}
|
||||
):
|
||||
# Perform a ``find`` operation that succeeds.
|
||||
client.test.test.find_one()
|
||||
|
||||
# Assert that the callback has been called twice.
|
||||
self.assertEqual(self.request_called, 2)
|
||||
|
||||
# Assert that the refresh token was used once.
|
||||
self.assertEqual(self.refresh_present, 1)
|
||||
|
||||
def test_3_1_uses_speculative_authentication_if_there_is_a_cached_token(self):
|
||||
# Create a client with a human callback that returns a valid token.
|
||||
client = self.create_client()
|
||||
@ -259,8 +308,8 @@ class TestAuthOIDCHuman(OIDCTestBase):
|
||||
# Set a fail point for ``saslStart`` commands.
|
||||
with self.fail_point(
|
||||
{
|
||||
"mode": "alwaysOn",
|
||||
"data": {"failCommands": ["saslStart"], "errorCode": 20},
|
||||
"mode": {"times": 1},
|
||||
"data": {"failCommands": ["saslStart"], "errorCode": 18},
|
||||
}
|
||||
):
|
||||
# Perform a ``find`` operation that succeeds
|
||||
@ -276,8 +325,8 @@ class TestAuthOIDCHuman(OIDCTestBase):
|
||||
# Set a fail point for ``saslStart`` commands.
|
||||
with self.fail_point(
|
||||
{
|
||||
"mode": "alwaysOn",
|
||||
"data": {"failCommands": ["saslStart"], "errorCode": 20},
|
||||
"mode": {"times": 1},
|
||||
"data": {"failCommands": ["saslStart"], "errorCode": 18},
|
||||
}
|
||||
):
|
||||
# Perform a ``find`` operation that fails.
|
||||
@ -378,8 +427,16 @@ class TestAuthOIDCHuman(OIDCTestBase):
|
||||
client.close()
|
||||
|
||||
def test_4_3_reauthenticate_succeeds_after_refresh_fails(self):
|
||||
# Create a client with a human callback that returns a valid token.
|
||||
client = self.create_client()
|
||||
# Create a default OIDC client with a human callback that returns an invalid refresh token
|
||||
cb = self.create_request_cb()
|
||||
|
||||
class CustomRequest(OIDCCallback):
|
||||
def fetch(self, *args, **kwargs):
|
||||
result = cb.fetch(*args, **kwargs)
|
||||
result.refresh_token = "bad"
|
||||
return result
|
||||
|
||||
client = self.create_client(request_cb=CustomRequest())
|
||||
|
||||
# Perform a find operation that succeeds.
|
||||
client.test.test.find_one()
|
||||
@ -390,38 +447,56 @@ class TestAuthOIDCHuman(OIDCTestBase):
|
||||
# Force a reauthenication using a fail point.
|
||||
with self.fail_point(
|
||||
{
|
||||
"mode": {"times": 2},
|
||||
"data": {"failCommands": ["find", "saslStart"], "errorCode": 391},
|
||||
"mode": {"times": 1},
|
||||
"data": {"failCommands": ["find"], "errorCode": 391},
|
||||
}
|
||||
):
|
||||
# Perform a find operation that succeeds.
|
||||
client.test.test.find_one()
|
||||
|
||||
# Assert that the human callback has been called 3 times.
|
||||
self.assertEqual(self.request_called, 3)
|
||||
# Assert that the human callback has been called 2 times.
|
||||
self.assertEqual(self.request_called, 2)
|
||||
|
||||
# Close the client.
|
||||
client.close()
|
||||
|
||||
def test_4_4_reauthenticate_fails(self):
|
||||
# Create a client with a human callback that returns a valid token.
|
||||
client = self.create_client()
|
||||
# Create a default OIDC client with a human callback that returns invalid refresh tokens and
|
||||
# Returns invalid access tokens after the first access.
|
||||
cb = self.create_request_cb()
|
||||
|
||||
class CustomRequest(OIDCCallback):
|
||||
fetch_called = 0
|
||||
|
||||
def fetch(self, *args, **kwargs):
|
||||
self.fetch_called += 1
|
||||
result = cb.fetch(*args, **kwargs)
|
||||
result.refresh_token = "bad"
|
||||
if self.fetch_called > 1:
|
||||
result.access_token = "bad"
|
||||
return result
|
||||
|
||||
client = self.create_client(request_cb=CustomRequest())
|
||||
|
||||
# Perform a find operation that succeeds (to force a speculative auth).
|
||||
client.test.test.find_one()
|
||||
# Assert that the human callback has been called once.
|
||||
self.assertEqual(self.request_called, 1)
|
||||
|
||||
# Force a reauthentication using a failCommand.
|
||||
with self.fail_point(
|
||||
{
|
||||
"mode": {"times": 3},
|
||||
"data": {"failCommands": ["find", "saslStart"], "errorCode": 391},
|
||||
"mode": {"times": 1},
|
||||
"data": {"failCommands": ["find"], "errorCode": 391},
|
||||
}
|
||||
):
|
||||
# Perform a find operation that fails.
|
||||
with self.assertRaises(OperationFailure):
|
||||
client.test.test.find_one()
|
||||
# Assert that the human callback has been called two times.
|
||||
self.assertEqual(self.request_called, 2)
|
||||
|
||||
# Assert that the human callback has been called three times.
|
||||
self.assertEqual(self.request_called, 3)
|
||||
|
||||
# Close the client.
|
||||
client.close()
|
||||
|
||||
@ -818,6 +893,33 @@ class TestAuthOIDCMachine(OIDCTestBase):
|
||||
# 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
|
||||
client = self.create_client()
|
||||
|
||||
# Set a fail point for ``saslStart`` commands.
|
||||
with self.fail_point(
|
||||
{
|
||||
"mode": {"times": 1},
|
||||
"data": {"failCommands": ["saslStart"], "errorCode": 20},
|
||||
}
|
||||
):
|
||||
# Perform a ``find`` operation that fails.
|
||||
with self.assertRaises(OperationFailure):
|
||||
client.test.test.find_one()
|
||||
|
||||
# Assert that the callback has been called once.
|
||||
self.assertEqual(self.request_called, 1)
|
||||
|
||||
# Perform a ``find`` operation that succeeds.
|
||||
client.test.test.find_one()
|
||||
|
||||
# Assert that the callback has been called once.
|
||||
self.assertEqual(self.request_called, 1)
|
||||
|
||||
# Close the client.
|
||||
client.close()
|
||||
|
||||
def test_4_reauthentication(self):
|
||||
# Create a ``MongoClient`` configured with a custom OIDC callback that
|
||||
# implements the provider logic.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user