diff --git a/.evergreen-functions.yml b/.evergreen-functions.yml index 641060194..00e95d8f1 100644 --- a/.evergreen-functions.yml +++ b/.evergreen-functions.yml @@ -1,6 +1,13 @@ variables: - &e2e_include_expansions_in_env include_expansions_in_env: + - cognito_user_pool_id + - cognito_workload_federation_client_id + - cognito_user_name + - cognito_workload_federation_client_secret + - cognito_user_password + - cognito_workload_url + - cognito_workload_user_id - ARTIFACTORY_PASSWORD - ARTIFACTORY_USERNAME - GRS_PASSWORD diff --git a/.evergreen-tasks.yml b/.evergreen-tasks.yml index 779547d94..b88435d77 100644 --- a/.evergreen-tasks.yml +++ b/.evergreen-tasks.yml @@ -1240,6 +1240,32 @@ tasks: commands: - func: e2e_test + # OIDC tests + - name: e2e_replica_set_oidc_m2m_group + tags: [ "patch-run" ] + commands: + - func: e2e_test + + - name: e2e_replica_set_oidc_m2m_user + tags: [ "patch-run" ] + commands: + - func: e2e_test + + - name: e2e_replica_set_oidc_workforce + tags: [ "patch-run" ] + commands: + - func: e2e_test + + - name: e2e_sharded_cluster_oidc_m2m_group + tags: [ "patch-run" ] + commands: + - func: e2e_test + + - name: e2e_sharded_cluster_oidc_m2m_user + tags: [ "patch-run" ] + commands: + - func: e2e_test + - name: e2e_search_community_basic tags: ["patch-run"] commands: diff --git a/.evergreen.yml b/.evergreen.yml index 940b5ac04..749dc39bd 100644 --- a/.evergreen.yml +++ b/.evergreen.yml @@ -759,6 +759,12 @@ task_groups: - e2e_replica_set_pv_resize - e2e_sharded_cluster_pv_resize - e2e_community_and_meko_replicaset_scale + # OIDC test group + - e2e_replica_set_oidc_m2m_group + - e2e_replica_set_oidc_m2m_user + - e2e_replica_set_oidc_workforce + - e2e_sharded_cluster_oidc_m2m_group + - e2e_sharded_cluster_oidc_m2m_user <<: *teardown_group # this task group contains just a one task, which is smoke testing whether the operator diff --git a/controllers/om/deployment.go b/controllers/om/deployment.go index 061164d7d..254816648 100644 --- a/controllers/om/deployment.go +++ b/controllers/om/deployment.go @@ -639,11 +639,22 @@ func (d Deployment) SetRoles(roles []mdbv1.MongoDbRole) { } func (d Deployment) GetRoles() []mdbv1.MongoDbRole { - val, ok := d["roles"].([]mdbv1.MongoDbRole) - if !ok { + roles, ok := d["roles"] + if !ok || roles == nil { return []mdbv1.MongoDbRole{} } - return val + + rolesBytes, err := json.Marshal(roles) + if err != nil { + return []mdbv1.MongoDbRole{} + } + + var result []mdbv1.MongoDbRole + if err := json.Unmarshal(rolesBytes, &result); err != nil { + return []mdbv1.MongoDbRole{} + } + + return result } // GetAgentVersion returns the current version of all Agents in the deployment. It's empty until the diff --git a/controllers/operator/authentication/authentication.go b/controllers/operator/authentication/authentication.go index 1744317cd..e9f89a293 100644 --- a/controllers/operator/authentication/authentication.go +++ b/controllers/operator/authentication/authentication.go @@ -264,6 +264,7 @@ func enableAgentAuthentication(conn om.Connection, opts Options, log *zap.Sugare // we then configure the agent authentication for that type mechanism := convertToMechanismOrPanic(opts.AgentMechanism, ac) + if err := ensureAgentAuthenticationIsConfigured(conn, opts, ac, mechanism, log); err != nil { return xerrors.Errorf("error ensuring agent authentication is configured: %w", err) } diff --git a/docker/mongodb-kubernetes-tests/kubetester/automation_config_tester.py b/docker/mongodb-kubernetes-tests/kubetester/automation_config_tester.py index 68c75198e..1481de6c1 100644 --- a/docker/mongodb-kubernetes-tests/kubetester/automation_config_tester.py +++ b/docker/mongodb-kubernetes-tests/kubetester/automation_config_tester.py @@ -97,6 +97,18 @@ def assert_processes_size(self, expected_size: int): def assert_sharding_size(self, expected_size: int): assert len(self.automation_config["sharding"]) == expected_size + def assert_oidc_providers_size(self, expected_size: int): + assert len(self.automation_config["oidcProviderConfigs"]) == expected_size + + def assert_oidc_configuration(self, oidc_config: Optional[Dict] = None): + actual_configs = self.automation_config["oidcProviderConfigs"] + assert len(actual_configs) == len( + oidc_config + ), f"Expected {len(oidc_config)} OIDC configs, but got {len(actual_configs)}" + + for expected, actual in zip(oidc_config, actual_configs): + assert expected == actual, f"Expected OIDC config: {expected}, but got: {actual}" + def assert_empty(self): self.assert_processes_size(0) self.assert_replica_sets_size(0) diff --git a/docker/mongodb-kubernetes-tests/kubetester/mongodb.py b/docker/mongodb-kubernetes-tests/kubetester/mongodb.py index e09a34df0..8febe799a 100644 --- a/docker/mongodb-kubernetes-tests/kubetester/mongodb.py +++ b/docker/mongodb-kubernetes-tests/kubetester/mongodb.py @@ -404,6 +404,41 @@ def get_authentication(self) -> Optional[Dict]: except KeyError: return {} + def get_oidc_provider_configs(self) -> Optional[Dict]: + try: + return self["spec"]["security"]["authentication"]["oidcProviderConfigs"] + except KeyError: + return {} + + def set_oidc_provider_configs(self, oidc_provider_configs: Dict): + self["spec"]["security"]["authentication"]["oidcProviderConfigs"] = oidc_provider_configs + return self + + def append_oidc_provider_config(self, new_config: Dict): + if "oidcProviderConfigs" not in self["spec"]["security"]["authentication"]: + self["spec"]["security"]["authentication"]["oidcProviderConfigs"] = [] + + oidc_configs = self["spec"]["security"]["authentication"]["oidcProviderConfigs"] + + oidc_configs.append(new_config) + + self["spec"]["security"]["authentication"]["oidcProviderConfigs"] = oidc_configs + + return self + + def get_roles(self) -> Optional[Dict]: + try: + return self["spec"]["security"]["roles"] + except KeyError: + return {} + + def append_role(self, new_role: Dict): + if "roles" not in self["spec"]["security"]: + self["spec"]["security"]["roles"] = [] + self["spec"]["security"]["roles"].append(new_role) + + return self + def get_authentication_modes(self) -> Optional[Dict]: try: return self.get_authentication()["modes"] diff --git a/docker/mongodb-kubernetes-tests/kubetester/mongotester.py b/docker/mongodb-kubernetes-tests/kubetester/mongotester.py index 9cca3119c..86ac0f75f 100644 --- a/docker/mongodb-kubernetes-tests/kubetester/mongotester.py +++ b/docker/mongodb-kubernetes-tests/kubetester/mongotester.py @@ -1,6 +1,7 @@ import copy import inspect import logging +import os import random import string import threading @@ -11,6 +12,8 @@ from kubetester import kubetester from kubetester.kubetester import KubernetesTester from opentelemetry import trace +from pycognito import Cognito +from pymongo.auth_oidc import OIDCCallback, OIDCCallbackContext, OIDCCallbackResult from pymongo.errors import OperationFailure, PyMongoError, ServerSelectionTimeoutError from pytest import fail @@ -61,6 +64,18 @@ def with_ldap(ssl_certfile: Optional[str] = None, tls_ca_file: Optional[str] = N return options +class MyOIDCCallback(OIDCCallback): + def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult: + u = Cognito( + user_pool_id=os.getenv("cognito_user_pool_id"), + client_id=os.getenv("cognito_workload_federation_client_id"), + username=os.getenv("cognito_user_name"), + client_secret=os.getenv("cognito_workload_federation_client_secret"), + ) + u.authenticate(password=os.getenv("cognito_user_password")) + return OIDCCallbackResult(access_token=u.id_token) + + class MongoTester: """MongoTester is a general abstraction to work with mongo database. It encapsulates the client created in the constructor. All general methods non-specific to types of mongodb topologies should reside here.""" @@ -277,6 +292,47 @@ def assert_ldap_authentication( fail(msg=f"unable to authenticate after {total_attempts} attempts") time.sleep(5) + def assert_oidc_authentication( + self, + db: str = "admin", + collection: str = "myCol", + attempts: int = 10, + ): + assert attempts > 0 + + props = {"OIDC_CALLBACK": MyOIDCCallback()} + + total_attempts = attempts + while True: + attempts -= 1 + try: + # Initialize the MongoDB client with OIDC authentication + self.client = self._init_client( + authMechanism="MONGODB-OIDC", + authMechanismProperties=props, + ) + # Perform a write operation to test authentication + self.client[db][collection].insert_one({"test": "oidc_auth_test"}) + return + except OperationFailure as e: + if attempts == 0: + raise RuntimeError(f"Unable to authenticate after {total_attempts} attempts: {e}") + time.sleep(5) + + def assert_oidc_authentication_fails(self, db: str = "admin", collection: str = "myCol", attempts: int = 10): + assert attempts > 0 + total_attempts = attempts + while True: + attempts -= 1 + try: + if attempts <= 0: + fail(msg=f"was able to authenticate with OIDC after {total_attempts} attempts") + + self.assert_oidc_authentication(db, collection, 1) + time.sleep(5) + except RuntimeError: + return + def upload_random_data( self, count: int, diff --git a/docker/mongodb-kubernetes-tests/kubetester/oidc.py b/docker/mongodb-kubernetes-tests/kubetester/oidc.py new file mode 100644 index 000000000..2e38f7e66 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/kubetester/oidc.py @@ -0,0 +1,26 @@ +import os + +# Note: 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. + +# A session explaining the setup can be found here: http://go/k8s-oidc-session + + +def get_cognito_workload_client_id() -> str: + return os.getenv("cognito_workload_federation_client_id", "") + + +def get_cognito_workload_url() -> str: + return os.getenv("cognito_workload_url", "") + + +def get_cognito_workload_user_id() -> str: + return os.getenv("cognito_workload_user_id", "") diff --git a/docker/mongodb-kubernetes-tests/tests/authentication/fixtures/oidc/oidc-user.yaml b/docker/mongodb-kubernetes-tests/tests/authentication/fixtures/oidc/oidc-user.yaml new file mode 100644 index 000000000..efc74882f --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/authentication/fixtures/oidc/oidc-user.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: mongodb.com/v1 +kind: MongoDBUser +metadata: + name: oidc-user-0 +spec: + username: "" + db: "$external" + mongodbResourceRef: + name: oidc-replica-set + roles: + - db: "admin" + name: "readWriteAnyDatabase" diff --git a/docker/mongodb-kubernetes-tests/tests/authentication/fixtures/oidc/replica-set-m2m-user.yaml b/docker/mongodb-kubernetes-tests/tests/authentication/fixtures/oidc/replica-set-m2m-user.yaml new file mode 100644 index 000000000..a09d50b9d --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/authentication/fixtures/oidc/replica-set-m2m-user.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: mongodb.com/v1 +kind: MongoDB +metadata: + name: oidc-replica-set +spec: + type: ReplicaSet + members: 3 + version: 7.0.5-ent + + opsManager: + configMapRef: + name: my-project + credentials: my-credentials + + security: + authentication: + agents: + mode: SCRAM + enabled: true + modes: + - SCRAM + - OIDC + oidcProviderConfigs: + - audience: "" + clientId: "" + issuerURI: "" + requestedScopes: [ ] + userClaim: "sub" + authorizationMethod: "WorkloadIdentityFederation" + authorizationType: "UserID" + configurationName: "OIDC-test-user" diff --git a/docker/mongodb-kubernetes-tests/tests/authentication/fixtures/oidc/replica-set-workforce.yaml b/docker/mongodb-kubernetes-tests/tests/authentication/fixtures/oidc/replica-set-workforce.yaml new file mode 100644 index 000000000..9b6a55ff8 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/authentication/fixtures/oidc/replica-set-workforce.yaml @@ -0,0 +1,47 @@ +--- +apiVersion: mongodb.com/v1 +kind: MongoDB +metadata: + name: oidc-replica-set +spec: + type: ReplicaSet + members: 3 + version: 7.0.5-ent + + opsManager: + configMapRef: + name: my-project + credentials: my-credentials + + security: + authentication: + agents: + mode: SCRAM + enabled: true + modes: + - SCRAM + - OIDC + oidcProviderConfigs: + - audience: "" + clientId: "" + issuerURI: "" + requestedScopes: [ ] + userClaim: "sub" + groupsClaim: "cognito:groups" + authorizationMethod: "WorkforceIdentityFederation" + authorizationType: "GroupMembership" + configurationName: "OIDC-test-group" + - audience: "dummy-audience" + clientId: "dummy-client-id" + issuerURI: "https://valid-issuer.example.com" + requestedScopes: [ ] + userClaim: "sub" + authorizationMethod: "WorkloadIdentityFederation" + authorizationType: "UserID" + configurationName: "OIDC-test-user" + roles: + - role: "OIDC-test-group/test" + db: "admin" + roles: + - role: "readWriteAnyDatabase" + db: "admin" diff --git a/docker/mongodb-kubernetes-tests/tests/authentication/fixtures/oidc/replica-set.yaml b/docker/mongodb-kubernetes-tests/tests/authentication/fixtures/oidc/replica-set.yaml new file mode 100644 index 000000000..da763de18 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/authentication/fixtures/oidc/replica-set.yaml @@ -0,0 +1,39 @@ +--- +apiVersion: mongodb.com/v1 +kind: MongoDB +metadata: + name: oidc-replica-set +spec: + type: ReplicaSet + members: 3 + version: 7.0.5-ent + + opsManager: + configMapRef: + name: my-project + credentials: my-credentials + + security: + authentication: + agents: + mode: SCRAM + enabled: true + modes: + - SCRAM + - OIDC + oidcProviderConfigs: + - audience: "" + clientId: "" + issuerURI: "" + requestedScopes: [ ] + userClaim: "sub" + groupsClaim: "cognito:groups" + authorizationMethod: "WorkloadIdentityFederation" + authorizationType: "GroupMembership" + configurationName: "OIDC-test" + roles: + - role: "OIDC-test/test" + db: "admin" + roles: + - role: "readWriteAnyDatabase" + db: "admin" diff --git a/docker/mongodb-kubernetes-tests/tests/authentication/fixtures/oidc/sharded-cluster-m2m-user.yaml b/docker/mongodb-kubernetes-tests/tests/authentication/fixtures/oidc/sharded-cluster-m2m-user.yaml new file mode 100644 index 000000000..379f396a4 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/authentication/fixtures/oidc/sharded-cluster-m2m-user.yaml @@ -0,0 +1,36 @@ +--- +apiVersion: mongodb.com/v1 +kind: MongoDB +metadata: + name: oidc-sharded-cluster-replica-set +spec: + type: ShardedCluster + shardCount: 2 + mongodsPerShardCount: 3 + mongosCount: 2 + configServerCount: 3 + version: 7.0.5-ent + + opsManager: + configMapRef: + name: my-project + credentials: my-credentials + + persistent: true + security: + authentication: + agents: + mode: SCRAM + enabled: true + modes: + - SCRAM + - OIDC + oidcProviderConfigs: + - audience: "" + clientId: "" + issuerURI: "" + requestedScopes: [ ] + userClaim: "sub" + authorizationMethod: "WorkloadIdentityFederation" + authorizationType: "UserID" + configurationName: "OIDC-test-user" diff --git a/docker/mongodb-kubernetes-tests/tests/authentication/fixtures/oidc/sharded-cluster-replica-set.yaml b/docker/mongodb-kubernetes-tests/tests/authentication/fixtures/oidc/sharded-cluster-replica-set.yaml new file mode 100644 index 000000000..ab4b4b231 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/authentication/fixtures/oidc/sharded-cluster-replica-set.yaml @@ -0,0 +1,52 @@ +--- +apiVersion: mongodb.com/v1 +kind: MongoDB +metadata: + name: oidc-sharded-cluster-replica-set +spec: + type: ShardedCluster + shardCount: 2 + mongodsPerShardCount: 3 + mongosCount: 2 + configServerCount: 3 + version: 7.0.5-ent + + opsManager: + configMapRef: + name: my-project + credentials: my-credentials + + persistent: true + security: + authentication: + agents: + mode: SCRAM + enabled: true + modes: + - SCRAM + - OIDC + oidcProviderConfigs: + - audience: "" + clientId: "" + issuerURI: "" + requestedScopes: [ ] + userClaim: "sub" + groupsClaim: "cognito:groups" + authorizationMethod: "WorkloadIdentityFederation" + authorizationType: "GroupMembership" + configurationName: "OIDC-test" + - audience: "test-audience" + clientId: "test-client-id" + issuerURI: "https://valid-issuer-1.example.com" + requestedScopes: [ ] + userClaim: "sub" + authorizationMethod: "WorkforceIdentityFederation" + authorizationType: "UserID" + configurationName: "OIDC-test-user" + + roles: + - role: "OIDC-test/test" + db: "admin" + roles: + - role: "readWriteAnyDatabase" + db: "admin" diff --git a/docker/mongodb-kubernetes-tests/tests/authentication/replica_set_oidc_m2m_group.py b/docker/mongodb-kubernetes-tests/tests/authentication/replica_set_oidc_m2m_group.py new file mode 100644 index 000000000..8f87ac003 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/authentication/replica_set_oidc_m2m_group.py @@ -0,0 +1,144 @@ +import kubetester.oidc as oidc +import pytest +from kubetester import find_fixture, try_load, wait_until +from kubetester.automation_config_tester import AutomationConfigTester +from kubetester.kubetester import KubernetesTester, ensure_ent_version +from kubetester.mongodb import MongoDB, Phase +from kubetester.mongotester import ReplicaSetTester +from pytest import fixture + +MDB_RESOURCE = "oidc-replica-set" + + +@fixture(scope="module") +def replica_set(namespace: str, custom_mdb_version: str) -> MongoDB: + resource = MongoDB.from_yaml(find_fixture("oidc/replica-set.yaml"), namespace=namespace) + if try_load(resource): + return resource + + oidc_provider_configs = resource.get_oidc_provider_configs() + oidc_provider_configs[0]["clientId"] = oidc.get_cognito_workload_client_id() + oidc_provider_configs[0]["audience"] = oidc.get_cognito_workload_client_id() + oidc_provider_configs[0]["issuerURI"] = oidc.get_cognito_workload_url() + + resource.set_oidc_provider_configs(oidc_provider_configs) + + return resource.update() + + +# Tests that one Workload Group membership works as expected. +@pytest.mark.e2e_replica_set_oidc_m2m_group +class TestCreateOIDCReplicaset(KubernetesTester): + + def test_create_replicaset(self, replica_set: MongoDB): + replica_set.assert_reaches_phase(Phase.Running, timeout=400) + + def test_assert_connectivity(self, replica_set: MongoDB): + tester = replica_set.tester() + tester.assert_oidc_authentication() + + def test_ops_manager_state_updated_correctly(self, replica_set: MongoDB): + tester = replica_set.get_automation_config_tester() + tester.assert_authentication_mechanism_enabled("MONGODB-OIDC", active_auth_mechanism=False) + tester.assert_authentication_enabled(2) + + tester.assert_expected_users(0) + tester.assert_authoritative_set(True) + + +# Adds a second workload group membership and associated role; automation config is verified +@pytest.mark.e2e_replica_set_oidc_m2m_group +class TestAddNewOIDCProviderAndRole(KubernetesTester): + def test_add_oidc_provider_and_role(self, replica_set: MongoDB): + replica_set.assert_reaches_phase(Phase.Running, timeout=400) + + replica_set.load() + + new_oidc_provider_config = { + "audience": "dummy-audience", + "issuerURI": "https://valid-issuer.example.com", + "requestedScopes": [], + "userClaim": "sub", + "groupsClaim": "group", + "authorizationMethod": "WorkloadIdentityFederation", + "authorizationType": "GroupMembership", + "configurationName": "dummy-oidc-config", + } + + new_role = { + "role": "dummy-oidc-config/test", + "db": "admin", + "roles": [{"role": "readWriteAnyDatabase", "db": "admin"}], + } + + replica_set.append_oidc_provider_config(new_oidc_provider_config) + replica_set.append_role(new_role) + + replica_set.update() + replica_set.assert_reaches_phase(Phase.Running, timeout=400) + + def config_and_roles_preserved() -> bool: + tester = replica_set.get_automation_config_tester() + try: + + tester.assert_authentication_mechanism_enabled("MONGODB-OIDC", active_auth_mechanism=False) + tester.assert_authentication_enabled(2) + tester.assert_expected_users(0) + tester.assert_has_expected_number_of_roles(expected_roles=2) + + expected_oidc_configs = [ + { + "audience": oidc.get_cognito_workload_client_id(), + "issuerUri": oidc.get_cognito_workload_url(), + "clientId": oidc.get_cognito_workload_client_id(), + "userClaim": "sub", + "groupsClaim": "cognito:groups", + "JWKSPollSecs": 0, + "authNamePrefix": "OIDC-test", + "supportsHumanFlows": False, + "useAuthorizationClaim": True, + }, + { + "audience": "dummy-audience", + "issuerUri": "https://valid-issuer.example.com", + "userClaim": "sub", + "groupsClaim": "group", + "JWKSPollSecs": 0, + "authNamePrefix": "dummy-oidc-config", + "supportsHumanFlows": False, + "useAuthorizationClaim": True, + }, + ] + + tester.assert_oidc_configuration(expected_oidc_configs) + return True + except AssertionError: + return False + + wait_until(config_and_roles_preserved, timeout=300, sleep=5) + + +# Tests the removal of all oidc configs and roles +@pytest.mark.e2e_replica_set_oidc_m2m_group +class TestOIDCRemoval(KubernetesTester): + def test_remove_oidc_provider_and_user(self, replica_set: MongoDB): + replica_set.assert_reaches_phase(Phase.Running, timeout=400) + + replica_set.load() + replica_set["spec"]["security"]["authentication"]["modes"] = ["SCRAM"] + replica_set["spec"]["security"]["authentication"]["oidcProviderConfigs"] = None + replica_set["spec"]["security"]["roles"] = None + + replica_set.update() + replica_set.assert_reaches_phase(Phase.Running, timeout=400) + + def config_updated() -> bool: + tester = replica_set.get_automation_config_tester() + try: + tester.assert_authentication_mechanism_enabled("SCRAM-SHA-256", active_auth_mechanism=False) + tester.assert_authentication_enabled(1) + return True + except AssertionError: + return False + + wait_until(config_updated, timeout=300, sleep=5) diff --git a/docker/mongodb-kubernetes-tests/tests/authentication/replica_set_oidc_m2m_user.py b/docker/mongodb-kubernetes-tests/tests/authentication/replica_set_oidc_m2m_user.py new file mode 100644 index 000000000..6765b2ce8 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/authentication/replica_set_oidc_m2m_user.py @@ -0,0 +1,111 @@ +import kubetester.oidc as oidc +import pytest +from kubetester import find_fixture, try_load, wait_until +from kubetester.automation_config_tester import AutomationConfigTester +from kubetester.kubetester import KubernetesTester, ensure_ent_version +from kubetester.mongodb import MongoDB, Phase +from kubetester.mongodb_user import MongoDBUser +from kubetester.mongotester import ReplicaSetTester +from pytest import fixture + +MDB_RESOURCE = "oidc-replica-set" +TEST_DATABASE = "myDB" + + +@fixture(scope="module") +def replica_set(namespace: str, custom_mdb_version: str) -> MongoDB: + resource = MongoDB.from_yaml(find_fixture("oidc/replica-set-m2m-user.yaml"), namespace=namespace) + if try_load(resource): + return resource + + oidc_provider_configs = resource.get_oidc_provider_configs() + oidc_provider_configs[0]["clientId"] = oidc.get_cognito_workload_client_id() + oidc_provider_configs[0]["audience"] = oidc.get_cognito_workload_client_id() + oidc_provider_configs[0]["issuerURI"] = oidc.get_cognito_workload_url() + + resource.set_oidc_provider_configs(oidc_provider_configs) + + return resource.update() + + +@fixture(scope="module") +def oidc_user(namespace) -> MongoDBUser: + resource = MongoDBUser.from_yaml(find_fixture("oidc/oidc-user.yaml"), namespace=namespace) + if try_load(resource): + return resource + + resource["spec"]["username"] = f"OIDC-test-user/{oidc.get_cognito_workload_user_id()}" + + return resource.update() + + +# Tests that one Workload Group membership works as expected. +@pytest.mark.e2e_replica_set_oidc_m2m_user +class TestCreateOIDCReplicaset(KubernetesTester): + + def test_create_replicaset(self, replica_set: MongoDB): + replica_set.assert_reaches_phase(Phase.Running, timeout=400) + + def test_create_user(self, oidc_user: MongoDBUser): + oidc_user.assert_reaches_phase(Phase.Updated, timeout=400) + + def test_assert_connectivity(self, replica_set: MongoDB): + tester = replica_set.tester() + tester.assert_oidc_authentication() + + def test_ops_manager_state_updated_correctly(self, replica_set: MongoDB): + tester = replica_set.get_automation_config_tester() + tester.assert_authentication_mechanism_enabled("MONGODB-OIDC", active_auth_mechanism=False) + tester.assert_authentication_enabled(2) + tester.assert_oidc_providers_size(1) + tester.assert_expected_users(1) + tester.assert_authoritative_set(True) + + +@pytest.mark.e2e_replica_set_oidc_m2m_user +class TestNewUserAdditionToReplicaSet(KubernetesTester): + def test_add_oidc_user(self, replica_set: MongoDB, namespace: str): + replica_set.assert_reaches_phase(Phase.Running, timeout=400) + + replica_set.load() + + new_oidc_user = MongoDBUser.from_yaml(find_fixture("oidc/oidc-user.yaml"), namespace=namespace) + new_oidc_user["metadata"]["name"] = "new-oidc-user" + new_oidc_user["spec"]["username"] = "OIDC-test-user/dummy-user-id" + + new_oidc_user.update() + new_oidc_user.assert_reaches_phase(Phase.Updated, timeout=400) + + def test_confirm_number_of_users(self, replica_set: MongoDB): + def assert_expected_users() -> bool: + tester = replica_set.get_automation_config_tester() + try: + tester.assert_expected_users(2) + return True + except AssertionError: + return False + + wait_until(assert_expected_users, timeout=300, sleep=5) + + +# Tests that database level roles are correctly applied to the user +@pytest.mark.e2e_replica_set_oidc_m2m_user +class TestRestrictedAccessToReplicaSet(KubernetesTester): + def test_update_oidc_user(self, replica_set: MongoDB, oidc_user: MongoDBUser, namespace: str): + oidc_user.load() + oidc_user["spec"]["roles"] = [{"db": TEST_DATABASE, "name": "readWrite"}] + oidc_user.update() + + oidc_user.assert_reaches_phase(Phase.Updated, timeout=400) + + def test_connection_with_specific_database(self, replica_set: MongoDB, oidc_user: MongoDBUser): + replica_set.assert_reaches_phase(Phase.Running, timeout=400) + tester = replica_set.tester() + + tester.assert_oidc_authentication(db=TEST_DATABASE) + + def test_connection_should_fail_with_other_database(self, replica_set: MongoDB, oidc_user: MongoDBUser): + replica_set.assert_reaches_phase(Phase.Running, timeout=400) + tester = replica_set.tester() + + tester.assert_oidc_authentication_fails() diff --git a/docker/mongodb-kubernetes-tests/tests/authentication/replica_set_oidc_workforce.py b/docker/mongodb-kubernetes-tests/tests/authentication/replica_set_oidc_workforce.py new file mode 100644 index 000000000..a331c96aa --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/authentication/replica_set_oidc_workforce.py @@ -0,0 +1,85 @@ +import kubetester.oidc as oidc +import pytest +from kubetester import try_load, wait_until +from kubetester.automation_config_tester import AutomationConfigTester +from kubetester.kubetester import KubernetesTester, ensure_ent_version +from kubetester.kubetester import fixture as load_fixture +from kubetester.mongodb import MongoDB, Phase +from kubetester.mongodb_user import MongoDBUser +from kubetester.mongotester import ReplicaSetTester +from pytest import fixture + +MDB_RESOURCE = "oidc-replica-set" + + +@fixture(scope="module") +def replica_set(namespace: str, custom_mdb_version: str) -> MongoDB: + resource = MongoDB.from_yaml(load_fixture("oidc/replica-set-workforce.yaml"), namespace=namespace) + if try_load(resource): + return resource + + oidc_provider_configs = resource.get_oidc_provider_configs() + + oidc_provider_configs[0]["clientId"] = oidc.get_cognito_workload_client_id() + oidc_provider_configs[0]["audience"] = oidc.get_cognito_workload_client_id() + oidc_provider_configs[0]["issuerURI"] = oidc.get_cognito_workload_url() + + resource.set_oidc_provider_configs(oidc_provider_configs) + + return resource.update() + + +@fixture(scope="module") +def oidc_user(namespace) -> MongoDBUser: + resource = MongoDBUser.from_yaml(load_fixture("oidc/oidc-user.yaml"), namespace=namespace) + if try_load(resource): + return resource + + resource["spec"]["username"] = f"OIDC-test-user/{oidc.get_cognito_workload_user_id()}" + + return resource.update() + + +@pytest.mark.e2e_replica_set_oidc_workforce +class TestCreateOIDCReplicaset(KubernetesTester): + + def test_create_replicaset(self, replica_set: MongoDB): + replica_set.assert_reaches_phase(Phase.Running, timeout=400) + + def test_create_user(self, oidc_user: MongoDBUser): + oidc_user.assert_reaches_phase(Phase.Updated, timeout=400) + + def test_ops_manager_state_updated_correctly(self, replica_set: MongoDB): + tester = replica_set.get_automation_config_tester() + tester.assert_authentication_mechanism_enabled("MONGODB-OIDC", active_auth_mechanism=False) + tester.assert_authentication_enabled(2) + tester.assert_oidc_providers_size(2) + + expected_oidc_configs = [ + { + "audience": oidc.get_cognito_workload_client_id(), + "issuerUri": oidc.get_cognito_workload_url(), + "clientId": oidc.get_cognito_workload_client_id(), + "userClaim": "sub", + "groupsClaim": "cognito:groups", + "JWKSPollSecs": 0, + "authNamePrefix": "OIDC-test-group", + "supportsHumanFlows": True, + "useAuthorizationClaim": True, + }, + { + "audience": "dummy-audience", + "issuerUri": "https://valid-issuer.example.com", + "clientId": "dummy-client-id", + "userClaim": "sub", + "JWKSPollSecs": 0, + "authNamePrefix": "OIDC-test-user", + "supportsHumanFlows": False, + "useAuthorizationClaim": False, + }, + ] + + tester.assert_oidc_configuration(expected_oidc_configs) + + tester.assert_expected_users(1) + tester.assert_authoritative_set(True) diff --git a/docker/mongodb-kubernetes-tests/tests/authentication/sharded_cluster_oidc_m2m_group.py b/docker/mongodb-kubernetes-tests/tests/authentication/sharded_cluster_oidc_m2m_group.py new file mode 100644 index 000000000..d6fa86936 --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/authentication/sharded_cluster_oidc_m2m_group.py @@ -0,0 +1,156 @@ +import kubetester.oidc as oidc +import pytest +from kubetester import find_fixture, try_load, wait_until +from kubetester.automation_config_tester import AutomationConfigTester +from kubetester.kubetester import KubernetesTester, ensure_ent_version +from kubetester.kubetester import fixture as load_fixture +from kubetester.mongodb import MongoDB, Phase +from kubetester.mongotester import ShardedClusterTester +from pytest import fixture + +MDB_RESOURCE = "oidc-sharded-cluster-replica-set" + + +@fixture(scope="module") +def sharded_cluster(namespace: str, custom_mdb_version: str) -> MongoDB: + resource = MongoDB.from_yaml(find_fixture("oidc/sharded-cluster-replica-set.yaml"), namespace=namespace) + if try_load(resource): + return resource + + oidc_provider_configs = resource.get_oidc_provider_configs() + + oidc_provider_configs[0]["clientId"] = oidc.get_cognito_workload_client_id() + oidc_provider_configs[0]["audience"] = oidc.get_cognito_workload_client_id() + oidc_provider_configs[0]["issuerURI"] = oidc.get_cognito_workload_url() + + resource.set_oidc_provider_configs(oidc_provider_configs) + + return resource.update() + + +@pytest.mark.e2e_sharded_cluster_oidc_m2m_group +class TestCreateOIDCShardedCluster(KubernetesTester): + + def test_create_sharded_cluster(self, sharded_cluster: MongoDB): + sharded_cluster.assert_reaches_phase(Phase.Running, timeout=600) + + def test_assert_connectivity(self, sharded_cluster: MongoDB): + tester = ShardedClusterTester(MDB_RESOURCE, 2) + tester.assert_oidc_authentication() + + def test_ops_manager_state_updated_correctly(self, sharded_cluster: MongoDB): + tester = sharded_cluster.get_automation_config_tester() + + tester.assert_authentication_mechanism_enabled("MONGODB-OIDC", active_auth_mechanism=False) + tester.assert_authentication_enabled(2) + tester.assert_oidc_providers_size(2) + tester.assert_expected_users(0) + tester.assert_has_expected_number_of_roles(expected_roles=1) + tester.assert_authoritative_set(True) + + expected_oidc_configs = [ + { + "audience": oidc.get_cognito_workload_client_id(), + "issuerUri": oidc.get_cognito_workload_url(), + "clientId": oidc.get_cognito_workload_client_id(), + "userClaim": "sub", + "groupsClaim": "cognito:groups", + "JWKSPollSecs": 0, + "authNamePrefix": "OIDC-test", + "supportsHumanFlows": False, + "useAuthorizationClaim": True, + }, + { + "audience": "test-audience", + "issuerUri": "https://valid-issuer-1.example.com", + "clientId": "test-client-id", + "userClaim": "sub", + "JWKSPollSecs": 0, + "authNamePrefix": "OIDC-test-user", + "supportsHumanFlows": True, + "useAuthorizationClaim": False, + }, + ] + tester.assert_oidc_configuration(expected_oidc_configs) + + +@pytest.mark.e2e_sharded_cluster_oidc_m2m_group +class TestAddNewOIDCProviderAndRole(KubernetesTester): + def test_add_oidc_provider_and_role(self, sharded_cluster: MongoDB): + sharded_cluster.assert_reaches_phase(Phase.Running, timeout=400) + + sharded_cluster.load() + + new_oidc_provider_config = { + "audience": "dummy-audience", + "issuerURI": "https://valid-issuer-2.example.com", + "requestedScopes": [], + "userClaim": "sub", + "groupsClaim": "group", + "authorizationMethod": "WorkloadIdentityFederation", + "authorizationType": "GroupMembership", + "configurationName": "dummy-oidc-config", + } + + new_role = { + "role": "dummy-oidc-config/test", + "db": "admin", + "roles": [{"role": "readWriteAnyDatabase", "db": "admin"}], + } + + sharded_cluster.append_oidc_provider_config(new_oidc_provider_config) + sharded_cluster.append_role(new_role) + + sharded_cluster.update() + sharded_cluster.assert_reaches_phase(Phase.Running, timeout=400) + + def config_and_roles_preserved() -> bool: + tester = sharded_cluster.get_automation_config_tester() + try: + tester.assert_authentication_mechanism_enabled("MONGODB-OIDC", active_auth_mechanism=False) + tester.assert_authentication_enabled(2) + tester.assert_expected_users(0) + tester.assert_has_expected_number_of_roles(expected_roles=2) + tester.assert_oidc_providers_size(3) + + # Updated configuration check with all 3 providers + expected_oidc_configs = [ + { + "audience": oidc.get_cognito_workload_client_id(), + "issuerUri": oidc.get_cognito_workload_url(), + "clientId": oidc.get_cognito_workload_client_id(), + "userClaim": "sub", + "groupsClaim": "cognito:groups", + "JWKSPollSecs": 0, + "authNamePrefix": "OIDC-test", + "supportsHumanFlows": False, + "useAuthorizationClaim": True, + }, + { + "audience": "test-audience", + "issuerUri": "https://valid-issuer-1.example.com", + "clientId": "test-client-id", + "userClaim": "sub", + "JWKSPollSecs": 0, + "authNamePrefix": "OIDC-test-user", + "supportsHumanFlows": True, + "useAuthorizationClaim": False, + }, + { + "audience": "dummy-audience", + "issuerUri": "https://valid-issuer-2.example.com", + "userClaim": "sub", + "groupsClaim": "group", + "JWKSPollSecs": 0, + "authNamePrefix": "dummy-oidc-config", + "supportsHumanFlows": False, + "useAuthorizationClaim": True, + }, + ] + + tester.assert_oidc_configuration(expected_oidc_configs) + return True + except AssertionError: + return False + + wait_until(config_and_roles_preserved, timeout=600, sleep=5) diff --git a/docker/mongodb-kubernetes-tests/tests/authentication/sharded_cluster_oidc_m2m_user.py b/docker/mongodb-kubernetes-tests/tests/authentication/sharded_cluster_oidc_m2m_user.py new file mode 100644 index 000000000..2a9cbbeee --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/authentication/sharded_cluster_oidc_m2m_user.py @@ -0,0 +1,76 @@ +import kubetester.oidc as oidc +import pytest +from kubetester import find_fixture, try_load +from kubetester.automation_config_tester import AutomationConfigTester +from kubetester.kubetester import KubernetesTester, ensure_ent_version +from kubetester.kubetester import fixture as load_fixture +from kubetester.mongodb import MongoDB, Phase +from kubetester.mongodb_user import MongoDBUser +from kubetester.mongotester import ShardedClusterTester +from pytest import fixture + +MDB_RESOURCE = "oidc-sharded-cluster-replica-set" + + +@fixture(scope="module") +def sharded_cluster(namespace: str, custom_mdb_version: str) -> MongoDB: + resource = MongoDB.from_yaml(find_fixture("oidc/sharded-cluster-m2m-user.yaml"), namespace=namespace) + + oidc_provider_configs = resource.get_oidc_provider_configs() + + oidc_provider_configs[0]["issuerURI"] = oidc.get_cognito_workload_url() + oidc_provider_configs[0]["clientId"] = oidc.get_cognito_workload_client_id() + oidc_provider_configs[0]["audience"] = oidc.get_cognito_workload_client_id() + + resource.set_oidc_provider_configs(oidc_provider_configs) + + if try_load(resource): + return resource + + return resource.update() + + +@fixture(scope="module") +def oidc_user(namespace) -> MongoDBUser: + resource = MongoDBUser.from_yaml(load_fixture("oidc/oidc-user.yaml"), namespace=namespace) + + resource["spec"]["username"] = f"OIDC-test-user/{oidc.get_cognito_workload_user_id()}" + resource["spec"]["mongodbResourceRef"]["name"] = MDB_RESOURCE + + return resource.update() + + +@pytest.mark.e2e_sharded_cluster_oidc_m2m_user +class TestCreateOIDCShardedCluster(KubernetesTester): + def test_create_sharded_cluster(self, sharded_cluster: MongoDB): + sharded_cluster.assert_reaches_phase(Phase.Running, timeout=600) + + def test_create_user(self, oidc_user: MongoDBUser): + oidc_user.assert_reaches_phase(Phase.Updated, timeout=400) + + def test_assert_connectivity(self, sharded_cluster: MongoDB): + tester = ShardedClusterTester(MDB_RESOURCE, 2) + tester.assert_oidc_authentication() + + def test_ops_manager_state_updated_correctly(self, sharded_cluster: MongoDB): + tester = sharded_cluster.get_automation_config_tester() + + tester.assert_authentication_mechanism_enabled("MONGODB-OIDC", active_auth_mechanism=False) + tester.assert_authentication_enabled(2) + tester.assert_oidc_providers_size(1) + tester.assert_expected_users(1) + tester.assert_authoritative_set(True) + + expected_oidc_configs = [ + { + "audience": oidc.get_cognito_workload_client_id(), + "issuerUri": oidc.get_cognito_workload_url(), + "clientId": oidc.get_cognito_workload_client_id(), + "userClaim": "sub", + "JWKSPollSecs": 0, + "authNamePrefix": "OIDC-test-user", + "supportsHumanFlows": False, + "useAuthorizationClaim": False, + }, + ] + tester.assert_oidc_configuration(expected_oidc_configs) diff --git a/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_mongodb_community.yaml b/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_mongodb_community.yaml new file mode 100644 index 000000000..340c810da --- /dev/null +++ b/docker/mongodb-kubernetes-tests/tests/webhooks/fixtures/invalid_oidc_mongodb_community.yaml @@ -0,0 +1,32 @@ +apiVersion: mongodb.com/v1 +kind: MongoDB +metadata: + name: oidc-replica-set +spec: + type: ReplicaSet + members: 3 + version: 8.0.5 + + opsManager: + configMapRef: + name: my-project + credentials: my-credentials + + security: + authentication: + agents: + mode: SCRAM + enabled: true + modes: + - SCRAM + - OIDC + oidcProviderConfigs: + - audience: "example-audience" + clientId: "example-client-id" + issuerURI: "https://example-issuer.com" + requestedScopes: [ ] + userClaim: "sub" + groupsClaim: "cognito:groups" + authorizationMethod: "WorkloadIdentityFederation" + authorizationType: "GroupMembership" + configurationName: "OIDC-test" diff --git a/requirements.txt b/requirements.txt index 20ad30f23..44fa0c412 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,9 +9,10 @@ semver==3.0.4 chardet==5.2.0 jsonpatch==1.33 kubernetes==30.1.0 -pymongo==4.12.1 +pymongo==4.13.0 pytest==8.3.5 pytest-asyncio==0.26.0 +pycognito==2024.5.1 PyYAML==6.0.2 urllib3==2.4.0 cryptography==45.0.3 diff --git a/scripts/dev/contexts/evg-private-context b/scripts/dev/contexts/evg-private-context index 52584edc7..f8080cbcb 100644 --- a/scripts/dev/contexts/evg-private-context +++ b/scripts/dev/contexts/evg-private-context @@ -112,3 +112,11 @@ export VERSION_UPGRADE_HOOK_IMAGE="268558157000.dkr.ecr.us-east-1.amazonaws.com/ # TODO to be removed at public preview stage of community-search export COMMUNITY_PRIVATE_PREVIEW_PULLSECRET_DOCKERCONFIGJSON="${community_private_preview_pullsecret_dockerconfigjson}" + +export cognito_user_pool_id="${cognito_user_pool_id}" +export cognito_workload_federation_client_id="${cognito_workload_federation_client_id}" +export cognito_user_name="${cognito_user_name}" +export cognito_workload_federation_client_secret="${cognito_workload_federation_client_secret}" +export cognito_user_password="${cognito_user_password}" +export cognito_workload_url="${cognito_workload_url}" +export cognito_workload_user_id="${cognito_workload_user_id}" diff --git a/scripts/evergreen/deployments/test-app/templates/mongodb-enterprise-tests.yaml b/scripts/evergreen/deployments/test-app/templates/mongodb-enterprise-tests.yaml index 4ef291daa..e108716b3 100644 --- a/scripts/evergreen/deployments/test-app/templates/mongodb-enterprise-tests.yaml +++ b/scripts/evergreen/deployments/test-app/templates/mongodb-enterprise-tests.yaml @@ -175,6 +175,20 @@ spec: {{ end }} - name: ops_manager_version value: "{{ .Values.opsManagerVersion }}" + - name: cognito_user_pool_id + value: {{ .Values.cognito_user_pool_id }} + - name: cognito_workload_federation_client_id + value: {{ .Values.cognito_workload_federation_client_id }} + - name: cognito_user_name + value: {{ .Values.cognito_user_name }} + - name: cognito_workload_federation_client_secret + value: {{ .Values.cognito_workload_federation_client_secret }} + - name: cognito_user_password + value: {{ .Values.cognito_user_password }} + - name: cognito_workload_url + value: {{ .Values.cognito_workload_url }} + - name: cognito_workload_user_id + value: {{ .Values.cognito_workload_user_id }} image: {{ .Values.repo }}/mongodb-kubernetes-tests:{{ .Values.tag }} # Options to pytest command should go in the pytest.ini file. command: ["pytest"] diff --git a/scripts/evergreen/e2e/single_e2e.sh b/scripts/evergreen/e2e/single_e2e.sh index 30c4770de..dbb1306da 100755 --- a/scripts/evergreen/e2e/single_e2e.sh +++ b/scripts/evergreen/e2e/single_e2e.sh @@ -53,6 +53,13 @@ deploy_test_app() { "--set" "mdbDefaultArchitecture=${MDB_DEFAULT_ARCHITECTURE:-'non-static'}" "--set" "mdbImageType=${MDB_IMAGE_TYPE:-'ubi8'}" "--set" "clusterDomain=${CLUSTER_DOMAIN:-'cluster.local'}" + "--set" "cognito_user_pool_id=${cognito_user_pool_id}" + "--set" "cognito_workload_federation_client_id=${cognito_workload_federation_client_id}" + "--set" "cognito_user_name=${cognito_user_name}" + "--set" "cognito_workload_federation_client_secret=${cognito_workload_federation_client_secret}" + "--set" "cognito_user_password=${cognito_user_password}" + "--set" "cognito_workload_url=${cognito_workload_url}" + "--set" "cognito_workload_user_id=${cognito_workload_user_id}" ) # shellcheck disable=SC2154