Skip to content

Commit 0f58ef3

Browse files
anandsyncsMaciejKaraslucian-tosa
authored
CLOUDP-314916: OIDC e2e test single cluster (#55)
# Summary ### Test File Summary - **replica_set_oidc_m2m_user.py** - Tests OIDC machine-to-machine (M2M) user authentication for a replica set. - Verifies user creation, role updates, access restrictions, and negative authentication scenarios. - **replica_set_oidc_m2m_group.py** - Tests OIDC group-based (Workload Identity Federation) authentication in a replica set. - Covers creation, OIDC provider/role updates, and removal of OIDC configs and roles. - **replica_set_oidc_workforce.py** - Tests OIDC workforce (human identity) authentication in a replica set. - Validates user creation and correct automation config for multiple OIDC providers. - **sharded_cluster_oidc_m2m_group.py** - Tests OIDC group-based authentication in a sharded cluster. - Verifies creation, connectivity, provider/role updates, and automation config state. - **sharded_cluster_oidc_m2m_user.py** - Tests OIDC machine-to-machine user authentication in a sharded cluster, including user creation, role assignment, and access restrictions. - Verifies correct OIDC provider configuration, user propagation, and negative authentication scenarios. ### Additional PR Changes - **OIDC Callback Integration:** Introduced a custom OIDC callback handler utilizing AWS Cognito for token acquisition, allowing automated OIDC authentication in E2E tests. - **Automation Config Tester Improvements:** Enhanced assertion helpers to validate OIDC-specific state in the Ops Manager automation config, including provider counts, configuration details, and user propagation. - **New OIDC Fixture Files:** Added YAML resource definitions for OIDC-enabled replica sets, sharded clusters, and MongoDB users, supporting a wide range of authentication and authorization test cases. - **Core Controller and Logic Adjustments:** Minor changes in Go controller code to ensure robust handling of OIDC provider configs and roles, and to support expanded test coverage. ### AWS Setup: The project uses AWS Cognito in the mongodb-mms-testing AWS account to facilitate OIDC authentication testing. This setup includes: - User Pool: A user pool in Cognito manages the identities. - Users: We use the user credentials to do authentication. - App Client: An app client is configured for machine-to-machine (M2M) authentication. - Groups: Cognito groups are used to manage users from the user pool for GroupMembership access. Environment variables and secrets required for these tests (like client IDs, URLs, and user IDs, as seen in the Python code) are stored in Evergreen and fetched from there during test execution. [Link to the session](http://go/k8s-oidc-session) where I explained the AWS setup for OIDC --- ## Proof of Work Added tests are passing. ## Checklist - [x] Have you linked a jira ticket and/or is the ticket in the title? - [ ] Have you checked whether your jira ticket required DOCSP changes? - [ ] Have you checked for release_note changes? ## Reminder (Please remove this when merging) - Please try to Approve or Reject Changes the PR, keep PRs in review as short as possible - Our Short Guide for PRs: [Link](https://docs.google.com/document/d/1T93KUtdvONq43vfTfUt8l92uo4e4SEEvFbIEKOxGr44/edit?tab=t.0) - Remember the following Communication Standards - use comment prefixes for clarity: * **blocking**: Must be addressed before approval. * **follow-up**: Can be addressed in a later PR or ticket. * **q**: Clarifying question. * **nit**: Non-blocking suggestions. * **note**: Side-note, non-actionable. Example: Praise * --> no prefix is considered a question --------- Co-authored-by: Maciej Karaś <maciej.karas@mongodb.com> Co-authored-by: Lucian Tosa <lucian.tosa@mongodb.com> Co-authored-by: Lucian Tosa <49226451+lucian-tosa@users.noreply.github.com>
1 parent f4465ab commit 0f58ef3

25 files changed

+1037
-4
lines changed

.evergreen-functions.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
variables:
22
- &e2e_include_expansions_in_env
33
include_expansions_in_env:
4+
- cognito_user_pool_id
5+
- cognito_workload_federation_client_id
6+
- cognito_user_name
7+
- cognito_workload_federation_client_secret
8+
- cognito_user_password
9+
- cognito_workload_url
10+
- cognito_workload_user_id
411
- ARTIFACTORY_PASSWORD
512
- ARTIFACTORY_USERNAME
613
- GRS_PASSWORD

.evergreen-tasks.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,6 +1240,32 @@ tasks:
12401240
commands:
12411241
- func: e2e_test
12421242

1243+
# OIDC tests
1244+
- name: e2e_replica_set_oidc_m2m_group
1245+
tags: [ "patch-run" ]
1246+
commands:
1247+
- func: e2e_test
1248+
1249+
- name: e2e_replica_set_oidc_m2m_user
1250+
tags: [ "patch-run" ]
1251+
commands:
1252+
- func: e2e_test
1253+
1254+
- name: e2e_replica_set_oidc_workforce
1255+
tags: [ "patch-run" ]
1256+
commands:
1257+
- func: e2e_test
1258+
1259+
- name: e2e_sharded_cluster_oidc_m2m_group
1260+
tags: [ "patch-run" ]
1261+
commands:
1262+
- func: e2e_test
1263+
1264+
- name: e2e_sharded_cluster_oidc_m2m_user
1265+
tags: [ "patch-run" ]
1266+
commands:
1267+
- func: e2e_test
1268+
12431269
- name: e2e_search_community_basic
12441270
tags: ["patch-run"]
12451271
commands:

.evergreen.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,12 @@ task_groups:
759759
- e2e_replica_set_pv_resize
760760
- e2e_sharded_cluster_pv_resize
761761
- e2e_community_and_meko_replicaset_scale
762+
# OIDC test group
763+
- e2e_replica_set_oidc_m2m_group
764+
- e2e_replica_set_oidc_m2m_user
765+
- e2e_replica_set_oidc_workforce
766+
- e2e_sharded_cluster_oidc_m2m_group
767+
- e2e_sharded_cluster_oidc_m2m_user
762768
<<: *teardown_group
763769

764770
# this task group contains just a one task, which is smoke testing whether the operator

controllers/om/deployment.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -639,11 +639,22 @@ func (d Deployment) SetRoles(roles []mdbv1.MongoDbRole) {
639639
}
640640

641641
func (d Deployment) GetRoles() []mdbv1.MongoDbRole {
642-
val, ok := d["roles"].([]mdbv1.MongoDbRole)
643-
if !ok {
642+
roles, ok := d["roles"]
643+
if !ok || roles == nil {
644644
return []mdbv1.MongoDbRole{}
645645
}
646-
return val
646+
647+
rolesBytes, err := json.Marshal(roles)
648+
if err != nil {
649+
return []mdbv1.MongoDbRole{}
650+
}
651+
652+
var result []mdbv1.MongoDbRole
653+
if err := json.Unmarshal(rolesBytes, &result); err != nil {
654+
return []mdbv1.MongoDbRole{}
655+
}
656+
657+
return result
647658
}
648659

649660
// GetAgentVersion returns the current version of all Agents in the deployment. It's empty until the

controllers/operator/authentication/authentication.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ func enableAgentAuthentication(conn om.Connection, opts Options, log *zap.Sugare
264264

265265
// we then configure the agent authentication for that type
266266
mechanism := convertToMechanismOrPanic(opts.AgentMechanism, ac)
267+
267268
if err := ensureAgentAuthenticationIsConfigured(conn, opts, ac, mechanism, log); err != nil {
268269
return xerrors.Errorf("error ensuring agent authentication is configured: %w", err)
269270
}

docker/mongodb-kubernetes-tests/kubetester/automation_config_tester.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,18 @@ def assert_processes_size(self, expected_size: int):
9797
def assert_sharding_size(self, expected_size: int):
9898
assert len(self.automation_config["sharding"]) == expected_size
9999

100+
def assert_oidc_providers_size(self, expected_size: int):
101+
assert len(self.automation_config["oidcProviderConfigs"]) == expected_size
102+
103+
def assert_oidc_configuration(self, oidc_config: Optional[Dict] = None):
104+
actual_configs = self.automation_config["oidcProviderConfigs"]
105+
assert len(actual_configs) == len(
106+
oidc_config
107+
), f"Expected {len(oidc_config)} OIDC configs, but got {len(actual_configs)}"
108+
109+
for expected, actual in zip(oidc_config, actual_configs):
110+
assert expected == actual, f"Expected OIDC config: {expected}, but got: {actual}"
111+
100112
def assert_empty(self):
101113
self.assert_processes_size(0)
102114
self.assert_replica_sets_size(0)

docker/mongodb-kubernetes-tests/kubetester/mongodb.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,41 @@ def get_authentication(self) -> Optional[Dict]:
404404
except KeyError:
405405
return {}
406406

407+
def get_oidc_provider_configs(self) -> Optional[Dict]:
408+
try:
409+
return self["spec"]["security"]["authentication"]["oidcProviderConfigs"]
410+
except KeyError:
411+
return {}
412+
413+
def set_oidc_provider_configs(self, oidc_provider_configs: Dict):
414+
self["spec"]["security"]["authentication"]["oidcProviderConfigs"] = oidc_provider_configs
415+
return self
416+
417+
def append_oidc_provider_config(self, new_config: Dict):
418+
if "oidcProviderConfigs" not in self["spec"]["security"]["authentication"]:
419+
self["spec"]["security"]["authentication"]["oidcProviderConfigs"] = []
420+
421+
oidc_configs = self["spec"]["security"]["authentication"]["oidcProviderConfigs"]
422+
423+
oidc_configs.append(new_config)
424+
425+
self["spec"]["security"]["authentication"]["oidcProviderConfigs"] = oidc_configs
426+
427+
return self
428+
429+
def get_roles(self) -> Optional[Dict]:
430+
try:
431+
return self["spec"]["security"]["roles"]
432+
except KeyError:
433+
return {}
434+
435+
def append_role(self, new_role: Dict):
436+
if "roles" not in self["spec"]["security"]:
437+
self["spec"]["security"]["roles"] = []
438+
self["spec"]["security"]["roles"].append(new_role)
439+
440+
return self
441+
407442
def get_authentication_modes(self) -> Optional[Dict]:
408443
try:
409444
return self.get_authentication()["modes"]

docker/mongodb-kubernetes-tests/kubetester/mongotester.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import copy
22
import inspect
33
import logging
4+
import os
45
import random
56
import string
67
import threading
@@ -11,6 +12,8 @@
1112
from kubetester import kubetester
1213
from kubetester.kubetester import KubernetesTester
1314
from opentelemetry import trace
15+
from pycognito import Cognito
16+
from pymongo.auth_oidc import OIDCCallback, OIDCCallbackContext, OIDCCallbackResult
1417
from pymongo.errors import OperationFailure, PyMongoError, ServerSelectionTimeoutError
1518
from pytest import fail
1619

@@ -61,6 +64,18 @@ def with_ldap(ssl_certfile: Optional[str] = None, tls_ca_file: Optional[str] = N
6164
return options
6265

6366

67+
class MyOIDCCallback(OIDCCallback):
68+
def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult:
69+
u = Cognito(
70+
user_pool_id=os.getenv("cognito_user_pool_id"),
71+
client_id=os.getenv("cognito_workload_federation_client_id"),
72+
username=os.getenv("cognito_user_name"),
73+
client_secret=os.getenv("cognito_workload_federation_client_secret"),
74+
)
75+
u.authenticate(password=os.getenv("cognito_user_password"))
76+
return OIDCCallbackResult(access_token=u.id_token)
77+
78+
6479
class MongoTester:
6580
"""MongoTester is a general abstraction to work with mongo database. It encapsulates the client created in
6681
the constructor. All general methods non-specific to types of mongodb topologies should reside here."""
@@ -277,6 +292,47 @@ def assert_ldap_authentication(
277292
fail(msg=f"unable to authenticate after {total_attempts} attempts")
278293
time.sleep(5)
279294

295+
def assert_oidc_authentication(
296+
self,
297+
db: str = "admin",
298+
collection: str = "myCol",
299+
attempts: int = 10,
300+
):
301+
assert attempts > 0
302+
303+
props = {"OIDC_CALLBACK": MyOIDCCallback()}
304+
305+
total_attempts = attempts
306+
while True:
307+
attempts -= 1
308+
try:
309+
# Initialize the MongoDB client with OIDC authentication
310+
self.client = self._init_client(
311+
authMechanism="MONGODB-OIDC",
312+
authMechanismProperties=props,
313+
)
314+
# Perform a write operation to test authentication
315+
self.client[db][collection].insert_one({"test": "oidc_auth_test"})
316+
return
317+
except OperationFailure as e:
318+
if attempts == 0:
319+
raise RuntimeError(f"Unable to authenticate after {total_attempts} attempts: {e}")
320+
time.sleep(5)
321+
322+
def assert_oidc_authentication_fails(self, db: str = "admin", collection: str = "myCol", attempts: int = 10):
323+
assert attempts > 0
324+
total_attempts = attempts
325+
while True:
326+
attempts -= 1
327+
try:
328+
if attempts <= 0:
329+
fail(msg=f"was able to authenticate with OIDC after {total_attempts} attempts")
330+
331+
self.assert_oidc_authentication(db, collection, 1)
332+
time.sleep(5)
333+
except RuntimeError:
334+
return
335+
280336
def upload_random_data(
281337
self,
282338
count: int,
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import os
2+
3+
# Note: The project uses AWS Cognito in the mongodb-mms-testing AWS account to facilitate OIDC authentication testing.
4+
# This setup includes:
5+
6+
# User Pool: A user pool in Cognito manages the identities.
7+
# Users: We use the user credentials to do authentication.
8+
# App Client: An app client is configured for machine-to-machine (M2M) authentication.
9+
# Groups: Cognito groups are used to manage users from the user pool for GroupMembership access.
10+
11+
# Environment variables and secrets required for these tests (like client IDs, URLs, and user IDs, as seen in the Python code)
12+
# are stored in Evergreen and fetched from there during test execution.
13+
14+
# A session explaining the setup can be found here: http://go/k8s-oidc-session
15+
16+
17+
def get_cognito_workload_client_id() -> str:
18+
return os.getenv("cognito_workload_federation_client_id", "")
19+
20+
21+
def get_cognito_workload_url() -> str:
22+
return os.getenv("cognito_workload_url", "")
23+
24+
25+
def get_cognito_workload_user_id() -> str:
26+
return os.getenv("cognito_workload_user_id", "")
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
apiVersion: mongodb.com/v1
3+
kind: MongoDBUser
4+
metadata:
5+
name: oidc-user-0
6+
spec:
7+
username: "<filled-in-test>"
8+
db: "$external"
9+
mongodbResourceRef:
10+
name: oidc-replica-set
11+
roles:
12+
- db: "admin"
13+
name: "readWriteAnyDatabase"
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
apiVersion: mongodb.com/v1
3+
kind: MongoDB
4+
metadata:
5+
name: oidc-replica-set
6+
spec:
7+
type: ReplicaSet
8+
members: 3
9+
version: 7.0.5-ent
10+
11+
opsManager:
12+
configMapRef:
13+
name: my-project
14+
credentials: my-credentials
15+
16+
security:
17+
authentication:
18+
agents:
19+
mode: SCRAM
20+
enabled: true
21+
modes:
22+
- SCRAM
23+
- OIDC
24+
oidcProviderConfigs:
25+
- audience: "<filled-in-test>"
26+
clientId: "<filled-in-test>"
27+
issuerURI: "<filled-in-test>"
28+
requestedScopes: [ ]
29+
userClaim: "sub"
30+
authorizationMethod: "WorkloadIdentityFederation"
31+
authorizationType: "UserID"
32+
configurationName: "OIDC-test-user"
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
apiVersion: mongodb.com/v1
3+
kind: MongoDB
4+
metadata:
5+
name: oidc-replica-set
6+
spec:
7+
type: ReplicaSet
8+
members: 3
9+
version: 7.0.5-ent
10+
11+
opsManager:
12+
configMapRef:
13+
name: my-project
14+
credentials: my-credentials
15+
16+
security:
17+
authentication:
18+
agents:
19+
mode: SCRAM
20+
enabled: true
21+
modes:
22+
- SCRAM
23+
- OIDC
24+
oidcProviderConfigs:
25+
- audience: "<filled-in-test>"
26+
clientId: "<filled-in-test>"
27+
issuerURI: "<filled-in-test>"
28+
requestedScopes: [ ]
29+
userClaim: "sub"
30+
groupsClaim: "cognito:groups"
31+
authorizationMethod: "WorkforceIdentityFederation"
32+
authorizationType: "GroupMembership"
33+
configurationName: "OIDC-test-group"
34+
- audience: "dummy-audience"
35+
clientId: "dummy-client-id"
36+
issuerURI: "https://valid-issuer.example.com"
37+
requestedScopes: [ ]
38+
userClaim: "sub"
39+
authorizationMethod: "WorkloadIdentityFederation"
40+
authorizationType: "UserID"
41+
configurationName: "OIDC-test-user"
42+
roles:
43+
- role: "OIDC-test-group/test"
44+
db: "admin"
45+
roles:
46+
- role: "readWriteAnyDatabase"
47+
db: "admin"

0 commit comments

Comments
 (0)