Skip to content

Commit 328fd6f

Browse files
Make mongodb work without admin privelege (#2023)
1 parent 64d9692 commit 328fd6f

File tree

2 files changed

+120
-54
lines changed

2 files changed

+120
-54
lines changed

connectors/sources/mongo.py

+39-13
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from bson import DBRef, Decimal128, ObjectId
1313
from fastjsonschema import JsonSchemaValueException
1414
from motor.motor_asyncio import AsyncIOMotorClient
15+
from pymongo.errors import OperationFailure
1516

1617
from connectors.filtering.validation import (
1718
AdvancedRulesValidator,
@@ -256,24 +257,49 @@ async def validate_config(self):
256257
await super().validate_config()
257258

258259
client = self.client
260+
261+
user = self.configuration["user"]
259262
configured_database_name = self.configuration["database"]
260263
configured_collection_name = self.configuration["collection"]
261264

262-
existing_database_names = await client.list_database_names()
265+
# First check if collection is accessible
266+
try:
267+
# This works on both standalone and Managed mongo in the same way
268+
await client[configured_database_name].validate_collection(
269+
configured_collection_name
270+
)
271+
return
272+
except OperationFailure:
273+
self._logger.warning(
274+
f"Unable to access '{configured_database_name}.{configured_collection_name}' as user '{user}'"
275+
)
276+
277+
# If it's not accessible, try to make a good user-friendly error message
278+
try:
279+
# We will try to access some databases/collections to give a friendly message
280+
# That will suggest the name of existing collection - but only if the user
281+
# that we use to log in into MongoDB has access to it
282+
existing_database_names = await client.list_database_names()
263283

264-
self._logger.debug(f"Existing databases: {existing_database_names}")
284+
self._logger.debug(f"Existing databases: {existing_database_names}")
265285

266-
if configured_database_name not in existing_database_names:
267-
msg = f"Database ({configured_database_name}) does not exist. Existing databases: {', '.join(existing_database_names)}"
268-
raise ConfigurableFieldValueError(msg)
286+
if configured_database_name not in existing_database_names:
287+
msg = f"Database '{configured_database_name}' does not exist. Existing databases: {', '.join(existing_database_names)}"
288+
raise ConfigurableFieldValueError(msg)
269289

270-
database = client[configured_database_name]
290+
database = client[configured_database_name]
271291

272-
existing_collection_names = await database.list_collection_names()
273-
self._logger.debug(
274-
f"Existing collections in {configured_database_name}: {existing_collection_names}"
275-
)
292+
existing_collection_names = await database.list_collection_names()
293+
self._logger.debug(
294+
f"Existing collections in {configured_database_name}: {existing_collection_names}"
295+
)
276296

277-
if configured_collection_name not in existing_collection_names:
278-
msg = f"Collection ({configured_collection_name}) does not exist within database {configured_database_name}. Existing collections: {', '.join(existing_collection_names)}"
279-
raise ConfigurableFieldValueError(msg)
297+
if configured_collection_name not in existing_collection_names:
298+
msg = f"Collection '{configured_collection_name}' does not exist within database '{configured_database_name}'. Existing collections: {', '.join(existing_collection_names)}"
299+
raise ConfigurableFieldValueError(msg)
300+
except OperationFailure as e:
301+
# This happens if the user has no access to operations to list collection/database names
302+
# Managed MongoDB never gets here, but if we're running against a standalone mongo
303+
# Then this code can trigger
304+
msg = f"Database '{configured_database_name}' or collection '{configured_collection_name}' is not accessible by user '{user}'. Verify that these database and collection exist, and specified user has access to it"
305+
raise ConfigurableFieldValueError(msg) from e

tests/sources/test_mongo.py

+81-41
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import pytest
1313
from bson import DBRef, ObjectId
1414
from bson.decimal128 import Decimal128
15-
from pymongo.errors import ServerSelectionTimeoutError
15+
from pymongo.errors import OperationFailure
1616

1717
from connectors.protocol import Filter
1818
from connectors.source import ConfigurableFieldValueError
@@ -261,7 +261,13 @@ def future_with_result(result):
261261

262262

263263
@pytest.mark.asyncio
264-
async def test_validate_config_when_database_name_invalid_then_raises_exception():
264+
@mock.patch(
265+
"motor.motor_asyncio.AsyncIOMotorDatabase.validate_collection",
266+
side_effect=OperationFailure("Unauthorized"),
267+
)
268+
async def test_validate_config_when_database_name_invalid_then_raises_exception(
269+
patch_validate_collection,
270+
):
265271
server_database_names = ["hello", "world"]
266272
configured_database_name = "something"
267273

@@ -283,7 +289,13 @@ async def test_validate_config_when_database_name_invalid_then_raises_exception(
283289

284290

285291
@pytest.mark.asyncio
286-
async def test_validate_config_when_collection_name_invalid_then_raises_exception():
292+
@mock.patch(
293+
"motor.motor_asyncio.AsyncIOMotorDatabase.validate_collection",
294+
side_effect=OperationFailure("Unauthorized"),
295+
)
296+
async def test_validate_config_when_collection_name_invalid_then_raises_exception(
297+
patch_validate_collection,
298+
):
287299
server_database_names = ["hello"]
288300
server_collection_names = ["first", "second"]
289301
configured_database_name = "hello"
@@ -310,25 +322,76 @@ async def test_validate_config_when_collection_name_invalid_then_raises_exceptio
310322

311323

312324
@pytest.mark.asyncio
313-
async def test_validate_config_when_configuration_valid_then_does_not_raise():
314-
server_database_names = ["hello"]
315-
server_collection_names = ["first", "second"]
325+
@mock.patch(
326+
"motor.motor_asyncio.AsyncIOMotorDatabase.validate_collection",
327+
side_effect=OperationFailure("Unauthorized"),
328+
)
329+
@mock.patch(
330+
"motor.motor_asyncio.AsyncIOMotorClient.list_database_names",
331+
return_value=future_with_result(["hello"]),
332+
)
333+
@mock.patch(
334+
"motor.motor_asyncio.AsyncIOMotorDatabase.list_collection_names",
335+
return_value=future_with_result(["first"]),
336+
)
337+
async def test_validate_config_when_collection_access_unauthorized(
338+
patch_validate_collection, patch_list_database_names, patch_list_collection_names
339+
):
316340
configured_database_name = "hello"
317341
configured_collection_name = "second"
318342

319-
with mock.patch(
320-
"motor.motor_asyncio.AsyncIOMotorClient.list_database_names",
321-
return_value=future_with_result(server_database_names),
322-
), mock.patch(
323-
"motor.motor_asyncio.AsyncIOMotorDatabase.list_collection_names",
324-
return_value=future_with_result(server_collection_names),
325-
):
343+
with pytest.raises(ConfigurableFieldValueError) as e:
344+
async with create_mongo_source(
345+
database=configured_database_name,
346+
collection=configured_collection_name,
347+
) as source:
348+
await source.validate_config()
349+
350+
assert e is not None
351+
352+
353+
@pytest.mark.asyncio
354+
@mock.patch(
355+
"motor.motor_asyncio.AsyncIOMotorDatabase.validate_collection",
356+
side_effect=OperationFailure("Unauthorized"),
357+
)
358+
@mock.patch(
359+
"motor.motor_asyncio.AsyncIOMotorClient.list_database_names",
360+
side_effect=OperationFailure("Unauthorized"),
361+
)
362+
async def test_validate_config_when_collection_access_unauthorized_and_no_admin_access(
363+
patch_validate_collection, patch_list_database_names
364+
):
365+
configured_database_name = "hello"
366+
configured_collection_name = "second"
367+
368+
with pytest.raises(ConfigurableFieldValueError) as e:
326369
async with create_mongo_source(
327370
database=configured_database_name,
328371
collection=configured_collection_name,
329372
) as source:
330373
await source.validate_config()
331374

375+
assert e is not None
376+
377+
378+
@pytest.mark.asyncio
379+
@mock.patch(
380+
"motor.motor_asyncio.AsyncIOMotorDatabase.validate_collection",
381+
return_value=future_with_result(None),
382+
)
383+
async def test_validate_config_when_configuration_valid_then_does_not_raise(
384+
patch_validate_connection,
385+
):
386+
configured_database_name = "hello"
387+
configured_collection_name = "second"
388+
389+
async with create_mongo_source(
390+
database=configured_database_name,
391+
collection=configured_collection_name,
392+
) as source:
393+
await source.validate_config()
394+
332395

333396
@pytest.mark.asyncio
334397
@pytest.mark.parametrize(
@@ -347,22 +410,6 @@ async def test_serialize(raw, output):
347410
assert source.serialize(raw) == output
348411

349412

350-
@pytest.mark.asyncio
351-
@mock.patch("ssl.SSLContext.load_verify_locations")
352-
async def test_ssl_connection_with_invalid_certificate(mock_ssl):
353-
mock_ssl.return_value = True
354-
async with create_mongo_source(
355-
ssl_enabled=True,
356-
ssl_ca="-----BEGIN CERTIFICATE----- Invalid-Certificate -----END CERTIFICATE-----",
357-
) as source:
358-
with mock.patch(
359-
"motor.motor_asyncio.AsyncIOMotorClient.list_database_names",
360-
side_effect=ServerSelectionTimeoutError(),
361-
):
362-
with pytest.raises(ServerSelectionTimeoutError):
363-
await source.validate_config()
364-
365-
366413
@pytest.mark.asyncio
367414
@pytest.mark.parametrize(
368415
"certificate_value, tls_insecure",
@@ -379,19 +426,12 @@ async def test_ssl_connection_with_invalid_certificate(mock_ssl):
379426
),
380427
],
381428
)
382-
@mock.patch("ssl.SSLContext.load_verify_locations")
429+
@mock.patch("ssl.SSLContext.load_verify_locations", return_value=True)
383430
async def test_ssl_with_successful_connection(
384431
mock_ssl, certificate_value, tls_insecure
385432
):
386-
mock_ssl.return_value = True
387433
async with create_mongo_source(
388-
ssl_enabled=True, ssl_ca=certificate_value, tls_insecure=tls_insecure
389-
) as source:
390-
with mock.patch(
391-
"motor.motor_asyncio.AsyncIOMotorClient.list_database_names",
392-
return_value=future_with_result(["db"]),
393-
), mock.patch(
394-
"motor.motor_asyncio.AsyncIOMotorDatabase.list_collection_names",
395-
return_value=future_with_result(["col"]),
396-
):
397-
await source.validate_config()
434+
ssl_enabled=True,
435+
ssl_ca="-----BEGIN CERTIFICATE----- Invalid-Certificate -----END CERTIFICATE-----",
436+
):
437+
assert True

0 commit comments

Comments
 (0)