Skip to content

Commit 3eea205

Browse files
authored
PYTHON-5786 Add test variant for the Secure Frontend Processor (monguard) (#2948)
1 parent af6235f commit 3eea205

8 files changed

Lines changed: 263 additions & 0 deletions

File tree

.evergreen/generated_configs/variants.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,17 @@ buildvariants:
564564
VERSION: latest
565565
tags: [coverage_tag]
566566

567+
# Sfp tests
568+
- name: sfp-rhel8
569+
tasks:
570+
- name: .test-no-orchestration
571+
display_name: SFP RHEL8
572+
run_on:
573+
- rhel87-small
574+
expansions:
575+
TEST_NAME: sfp
576+
tags: [pr]
577+
567578
# Stable api tests
568579
- name: stable-api-require-v1-rhel8-auth
569580
tasks:

.evergreen/scripts/generate_config.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,19 @@ def create_atlas_connect_variants():
464464
]
465465

466466

467+
def create_sfp_variants():
468+
host = DEFAULT_HOST
469+
return [
470+
create_variant(
471+
[".test-no-orchestration"],
472+
get_variant_name("SFP", host),
473+
tags=["pr"],
474+
host=host,
475+
expansions=dict(TEST_NAME="sfp"),
476+
)
477+
]
478+
479+
467480
def create_coverage_report_variants():
468481
return [create_variant(["coverage-report"], "Coverage Report", host=DEFAULT_HOST)]
469482

.evergreen/scripts/setup_tests.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,19 @@ def handle_test_env() -> None:
436436
# We do not want the default client_context to be initialized.
437437
write_env("DISABLE_CONTEXT")
438438

439+
if test_name == "sfp":
440+
secrets = get_secrets("drivers/sfp")
441+
442+
# Write file with SFP Atlas X509 client certificate:
443+
decoded = base64.b64decode(secrets["SFP_ATLAS_X509_BASE64"]).decode("utf8")
444+
cert_file = ROOT / ".evergreen/atlas_x509_sfp_client_certificate.pem"
445+
with cert_file.open("w") as file:
446+
file.write(decoded)
447+
write_env("SFP_ATLAS_X509_CERT", str(cert_file))
448+
449+
# We do not want the default client_context to be initialized.
450+
write_env("DISABLE_CONTEXT")
451+
439452
if test_name == "numpy":
440453
UV_ARGS.append("--with numpy")
441454

.evergreen/scripts/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class Distro:
4444
"mockupdb": "mockupdb",
4545
"ocsp": "ocsp",
4646
"perf": "perf",
47+
"sfp": "sfp",
4748
"numpy": "",
4849
}
4950

@@ -59,6 +60,7 @@ class Distro:
5960
"aws_lambda",
6061
"mockupdb",
6162
"ocsp",
63+
"sfp",
6264
]
6365

6466
# Mapping of env variables to options

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ specifications/
3232
results.json
3333
.synchro-modified
3434
.evergreen/atlas_x509_dev_client_certificate.pem
35+
.evergreen/atlas_x509_sfp_client_certificate.pem
3536

3637
# Lambda temp files
3738
test/lambda/.aws-sam

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ markers = [
138138
"auth: tests that rely on authentication",
139139
"ocsp: tests that rely on ocsp",
140140
"atlas_connect: tests that rely on an atlas connection",
141+
"sfp: tests that rely on an Atlas Secure Frontend Processor connection",
141142
"perf: benchmark tests",
142143
"search_index: search index helper tests",
143144
"kms: client-side field-level encryption tests using kms",

test/asynchronous/test_sfp.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Copyright 2026-present MongoDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Test connectivity and authentication through an Atlas Secure Frontend Processor (SFP/Monguard)."""
16+
17+
from __future__ import annotations
18+
19+
import os
20+
import sys
21+
import unittest
22+
from typing import Any
23+
24+
import pytest
25+
26+
sys.path[0:0] = [""]
27+
28+
from bson import ObjectId
29+
from pymongo.asynchronous.mongo_client import AsyncMongoClient
30+
from pymongo.server_api import ServerApi
31+
from test.asynchronous import AsyncPyMongoTestCase
32+
33+
_IS_SYNC = False
34+
35+
pytestmark = pytest.mark.sfp
36+
37+
# Each authenticated test must run under each of these variations:
38+
# no additional configuration, a compressor enabled, and Server API v1.
39+
VARIATIONS: dict[str, dict[str, Any]] = {
40+
"baseline": {},
41+
"compressor": {"compressors": "zlib"},
42+
"server_api": {"server_api": ServerApi("1")},
43+
}
44+
45+
46+
def _require_env(name: str) -> str:
47+
value = os.environ.get(name)
48+
if not value:
49+
raise Exception(f"Must set {name} env variable to test.")
50+
return value
51+
52+
53+
class TestAtlasSFP(AsyncPyMongoTestCase):
54+
async def assert_ping(self, client: AsyncMongoClient) -> None:
55+
response = await client.admin.command("ping")
56+
self.assertEqual(response["ok"], 1)
57+
58+
async def assert_connection_status(self, client: AsyncMongoClient, authenticated: bool) -> None:
59+
response = await client.admin.command("connectionStatus")
60+
self.assertEqual(response["ok"], 1)
61+
users = response["authInfo"]["authenticatedUsers"]
62+
if authenticated:
63+
self.assertGreaterEqual(len(users), 1)
64+
else:
65+
self.assertEqual(users, [])
66+
67+
async def assert_crud(self, client: AsyncMongoClient) -> None:
68+
# Use a unique collection name for each test run and drop it
69+
# afterward, regardless of test success or failure.
70+
collection = client.db[f"sfp_test_{ObjectId()}"]
71+
self.addAsyncCleanup(collection.drop)
72+
result = await collection.insert_one({"_id": 0})
73+
self.assertEqual(result.inserted_id, 0)
74+
document = await collection.find_one({"_id": 0})
75+
self.assertEqual(document, {"_id": 0})
76+
77+
async def test_unauthenticated(self):
78+
client = self.simple_client(_require_env("SFP_ATLAS_URI"))
79+
await self.assert_ping(client)
80+
await self.assert_connection_status(client, authenticated=False)
81+
82+
async def test_scram_sha_256(self):
83+
uri = _require_env("SFP_ATLAS_URI")
84+
username = _require_env("SFP_ATLAS_USER")
85+
password = _require_env("SFP_ATLAS_PASSWORD")
86+
for variation, kwargs in VARIATIONS.items():
87+
with self.subTest(variation=variation):
88+
client = self.simple_client(
89+
uri,
90+
username=username,
91+
password=password,
92+
authMechanism="SCRAM-SHA-256",
93+
**kwargs,
94+
)
95+
await self.assert_ping(client)
96+
await self.assert_connection_status(client, authenticated=True)
97+
await self.assert_crud(client)
98+
99+
async def test_x509(self):
100+
uri = _require_env("SFP_ATLAS_X509_URI")
101+
cert = _require_env("SFP_ATLAS_X509_CERT")
102+
for variation, kwargs in VARIATIONS.items():
103+
with self.subTest(variation=variation):
104+
client = self.simple_client(uri, tlsCertificateKeyFile=cert, **kwargs)
105+
await self.assert_ping(client)
106+
await self.assert_connection_status(client, authenticated=True)
107+
await self.assert_crud(client)
108+
109+
110+
if __name__ == "__main__":
111+
unittest.main()

test/test_sfp.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Copyright 2026-present MongoDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Test connectivity and authentication through an Atlas Secure Frontend Processor (SFP/Monguard)."""
16+
17+
from __future__ import annotations
18+
19+
import os
20+
import sys
21+
import unittest
22+
from typing import Any
23+
24+
import pytest
25+
26+
sys.path[0:0] = [""]
27+
28+
from bson import ObjectId
29+
from pymongo.server_api import ServerApi
30+
from pymongo.synchronous.mongo_client import MongoClient
31+
from test import PyMongoTestCase
32+
33+
_IS_SYNC = True
34+
35+
pytestmark = pytest.mark.sfp
36+
37+
# Each authenticated test must run under each of these variations:
38+
# no additional configuration, a compressor enabled, and Server API v1.
39+
VARIATIONS: dict[str, dict[str, Any]] = {
40+
"baseline": {},
41+
"compressor": {"compressors": "zlib"},
42+
"server_api": {"server_api": ServerApi("1")},
43+
}
44+
45+
46+
def _require_env(name: str) -> str:
47+
value = os.environ.get(name)
48+
if not value:
49+
raise Exception(f"Must set {name} env variable to test.")
50+
return value
51+
52+
53+
class TestAtlasSFP(PyMongoTestCase):
54+
def assert_ping(self, client: MongoClient) -> None:
55+
response = client.admin.command("ping")
56+
self.assertEqual(response["ok"], 1)
57+
58+
def assert_connection_status(self, client: MongoClient, authenticated: bool) -> None:
59+
response = client.admin.command("connectionStatus")
60+
self.assertEqual(response["ok"], 1)
61+
users = response["authInfo"]["authenticatedUsers"]
62+
if authenticated:
63+
self.assertGreaterEqual(len(users), 1)
64+
else:
65+
self.assertEqual(users, [])
66+
67+
def assert_crud(self, client: MongoClient) -> None:
68+
# Use a unique collection name for each test run and drop it
69+
# afterward, regardless of test success or failure.
70+
collection = client.db[f"sfp_test_{ObjectId()}"]
71+
self.addCleanup(collection.drop)
72+
result = collection.insert_one({"_id": 0})
73+
self.assertEqual(result.inserted_id, 0)
74+
document = collection.find_one({"_id": 0})
75+
self.assertEqual(document, {"_id": 0})
76+
77+
def test_unauthenticated(self):
78+
client = self.simple_client(_require_env("SFP_ATLAS_URI"))
79+
self.assert_ping(client)
80+
self.assert_connection_status(client, authenticated=False)
81+
82+
def test_scram_sha_256(self):
83+
uri = _require_env("SFP_ATLAS_URI")
84+
username = _require_env("SFP_ATLAS_USER")
85+
password = _require_env("SFP_ATLAS_PASSWORD")
86+
for variation, kwargs in VARIATIONS.items():
87+
with self.subTest(variation=variation):
88+
client = self.simple_client(
89+
uri,
90+
username=username,
91+
password=password,
92+
authMechanism="SCRAM-SHA-256",
93+
**kwargs,
94+
)
95+
self.assert_ping(client)
96+
self.assert_connection_status(client, authenticated=True)
97+
self.assert_crud(client)
98+
99+
def test_x509(self):
100+
uri = _require_env("SFP_ATLAS_X509_URI")
101+
cert = _require_env("SFP_ATLAS_X509_CERT")
102+
for variation, kwargs in VARIATIONS.items():
103+
with self.subTest(variation=variation):
104+
client = self.simple_client(uri, tlsCertificateKeyFile=cert, **kwargs)
105+
self.assert_ping(client)
106+
self.assert_connection_status(client, authenticated=True)
107+
self.assert_crud(client)
108+
109+
110+
if __name__ == "__main__":
111+
unittest.main()

0 commit comments

Comments
 (0)