diff --git a/modules/mongodb/src/main/java/org/testcontainers/containers/MongoDBContainer.java b/modules/mongodb/src/main/java/org/testcontainers/containers/MongoDBContainer.java
index d6304ac8c5d..455f68c7413 100644
--- a/modules/mongodb/src/main/java/org/testcontainers/containers/MongoDBContainer.java
+++ b/modules/mongodb/src/main/java/org/testcontainers/containers/MongoDBContainer.java
@@ -1,6 +1,8 @@
package org.testcontainers.containers;
import com.github.dockerjava.api.command.InspectContainerResponse;
+import lombok.Builder;
+import lombok.Getter;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@@ -10,7 +12,7 @@
import java.io.IOException;
/**
- * Constructs a single node MongoDB replica set for testing transactions.
+ * Constructs a single node MongoDB replica set for testing transactions etc. Please, use MongoDBContainer's methods to construct a URL connection string.
*
To construct a multi-node MongoDB cluster, consider the mongodb-replica-set project on GitHub
*
Tested on a MongoDB version 4.0.10+ (that is the default version if not specified).
*/
@@ -27,7 +29,22 @@ public class MongoDBContainer extends GenericContainer {
private static final int AWAIT_INIT_REPLICA_SET_ATTEMPTS = 60;
- private static final String MONGODB_DATABASE_NAME_DEFAULT = "test";
+ static final String DEFAULT_DATABASE_NAME = "test";
+
+ private static final String DEFAULT_USER = "test";
+
+ private static final String DEFAULT_PASSWORD = "test";
+
+ static final String DEFAULT_AUTHENTICATION_DATABASE_NAME = "admin";
+
+ private static final String AUTHENTICATION_KEY_FILE_NAME = "keyFile.key";
+
+ private static final String AUTHENTICATION_KEY_FILE_NAME_CONTAINER_PATH =
+ "/usr/local/bin/" + AUTHENTICATION_KEY_FILE_NAME;
+
+ private String username = DEFAULT_USER;
+
+ private String password = DEFAULT_PASSWORD;
/**
* @deprecated use {@link MongoDBContainer(DockerImageName)} instead
@@ -44,10 +61,40 @@ public MongoDBContainer(@NonNull final String dockerImageName) {
public MongoDBContainer(final DockerImageName dockerImageName) {
super(dockerImageName);
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
-
withExposedPorts(MONGODB_INTERNAL_PORT);
- withCommand("--replSet", "docker-rs");
- waitingFor(Wait.forLogMessage("(?i).*waiting for connections.*", 1));
+ withClasspathResourceMapping(
+ AUTHENTICATION_KEY_FILE_NAME,
+ AUTHENTICATION_KEY_FILE_NAME_CONTAINER_PATH,
+ BindMode.READ_ONLY
+ );
+ waitingFor(Wait.forLogMessage("(?i).*waiting for connections.*", 2));
+ }
+
+ @Override
+ protected void configure() {
+ addEnv("MONGO_INITDB_ROOT_USERNAME", this.username);
+ addEnv("MONGO_INITDB_ROOT_PASSWORD", this.password);
+ withCreateContainerCmdModifier(it -> it.withEntrypoint("bash"));
+ setCommand(
+ "-c",
+ "chown mongodb " +
+ AUTHENTICATION_KEY_FILE_NAME_CONTAINER_PATH +
+ ";chmod 400 " +
+ AUTHENTICATION_KEY_FILE_NAME_CONTAINER_PATH +
+ ";/usr/local/bin/docker-entrypoint.sh --keyFile " +
+ AUTHENTICATION_KEY_FILE_NAME_CONTAINER_PATH +
+ " --replSet docker-rs"
+ );
+ }
+
+ public MongoDBContainer withUsername(final String username) {
+ this.username = username;
+ return self();
+ }
+
+ public MongoDBContainer withPassword(final String password) {
+ this.password = password;
+ return self();
}
/**
@@ -56,16 +103,18 @@ public MongoDBContainer(final DockerImageName dockerImageName) {
* @return a connection url pointing to a mongodb instance
*/
public String getConnectionString() {
- return String.format("mongodb://%s:%d", getHost(), getMappedPort(MONGODB_INTERNAL_PORT));
+ return constructConnectionString(
+ ConnectionString.builder().username(this.username).password(this.password).build()
+ );
}
/**
- * Gets a replica set url for the default {@value #MONGODB_DATABASE_NAME_DEFAULT} database.
+ * Gets a replica set url for the default {@value #DEFAULT_DATABASE_NAME} database.
*
* @return a replica set url.
*/
public String getReplicaSetUrl() {
- return getReplicaSetUrl(MONGODB_DATABASE_NAME_DEFAULT);
+ return getReplicaSetUrl(ConnectionString.builder().username(this.username).password(this.password).build());
}
/**
@@ -78,7 +127,27 @@ public String getReplicaSetUrl(final String databaseName) {
if (!isRunning()) {
throw new IllegalStateException("MongoDBContainer should be started first");
}
- return getConnectionString() + "/" + databaseName;
+ return constructConnectionString(
+ ConnectionString
+ .builder()
+ .databaseName(databaseName)
+ .username(this.username)
+ .password(this.password)
+ .build()
+ );
+ }
+
+ /**
+ * Gets a replica set url for a provided {@link org.testcontainers.containers.MongoDBContainer.ConnectionString}.
+ *
+ * @param connectionString an object describing a connection string.
+ * @return a replica set url.
+ */
+ public String getReplicaSetUrl(final ConnectionString connectionString) {
+ if (!isRunning()) {
+ throw new IllegalStateException("MongoDBContainer should be started first");
+ }
+ return constructConnectionString(connectionString);
}
@Override
@@ -91,10 +160,25 @@ protected void containerIsStarted(InspectContainerResponse containerInfo, boolea
}
private String[] buildMongoEvalCommand(final String command) {
+ final String authOptions =
+ " -u " +
+ this.username +
+ " -p " +
+ this.password +
+ " --authenticationDatabase " +
+ DEFAULT_AUTHENTICATION_DATABASE_NAME;
return new String[] {
"sh",
"-c",
- "mongosh mongo --eval \"" + command + "\" || mongo --eval \"" + command + "\"",
+ "mongosh " +
+ authOptions +
+ " --eval \"" +
+ command +
+ "\" || mongo " +
+ authOptions +
+ " --eval \"" +
+ command +
+ "\"",
};
}
@@ -149,6 +233,18 @@ private void initReplicaSet() {
checkMongoNodeExitCodeAfterWaiting(execResultWaitForMaster);
}
+ private String constructConnectionString(final ConnectionString connectionString) {
+ return String.format(
+ "mongodb://%s:%s@%s:%d/%s?authSource=%s",
+ connectionString.getUsername(),
+ connectionString.getPassword(),
+ getHost(),
+ getMappedPort(MONGODB_INTERNAL_PORT),
+ connectionString.getDatabaseName(),
+ DEFAULT_AUTHENTICATION_DATABASE_NAME
+ );
+ }
+
public static class ReplicaSetInitializationException extends RuntimeException {
ReplicaSetInitializationException(final String errorMessage) {
@@ -164,4 +260,16 @@ private boolean isReplicationSetAlreadyInitialized() {
);
return execCheckRsInit.getExitCode() == CONTAINER_EXIT_CODE_OK;
}
+
+ @Builder
+ @Getter
+ public static class ConnectionString {
+
+ @Builder.Default
+ private final String databaseName = DEFAULT_DATABASE_NAME;
+
+ private final String username;
+
+ private final String password;
+ }
}
diff --git a/modules/mongodb/src/main/resources/keyFile.key b/modules/mongodb/src/main/resources/keyFile.key
new file mode 100644
index 00000000000..e3a178294b7
--- /dev/null
+++ b/modules/mongodb/src/main/resources/keyFile.key
@@ -0,0 +1,16 @@
+privateOldE8+8pPloNORolrRpGU6E6Ps8rh6GAcThtIfnh0Nfiy+fGvROwqlEtFmAY4Xb6
+aDjb/cFoYSNIxh5DZaBD4hmllCkoAAl15WDKWEv+ELxj124KiyuzJUbu50iXYG+/
+g6oWzElAdnckXCj+9CVhuw1dq9LgaIOd3n7NsrKK4rG7BgrdRl3HSpexBgd4WPva
+jtIcvg+eKBvUysACGSpCubFQa1VoRiU7G0h5TYPXVBpmvN7cFHKANNKyggDPKlih
+SfnMuXPGaecBm9UkmpHJoPUuzrE5wXStbho6SQzlbSBoxxgOCAHaAPtn7d3scP1i
+lk8hoIyNjbq37D2b15VP9+JxBgkqywjcm3Z7D5m+NSI22xYD44kNBxvAIFUtE1RS
+qgTFizA2ORb73TGfhhy2vuIJdsn97dZAMFOayiJvdzyIQJ9027d5eAVUE/U9UQjP
+7BrHrJ+iV+PwggmIvwXDjFP7n0gs6tGmghfG/13y3lwpD+Xs84hcEbXitdns+8dE
+lpnTkqUpGMexeuEuL4O5yfX46mVyT6+qvD+jb6y5oB1ydP/n3dmuWfoE3hv2rvVn
+pFbPzuTF2mvIj0HTTmkNBCBh8Rq7McZ2vNW5nx3jdf8A+ICw6O9KlkemhHORIsIY
+/HbsL1xjPs+gizMOddFwgfLovkQ9Oap7fAed+yl8JxqTWe5OMHZCDxyssZEOscnZ
+xSWEXKWsWv2LLqtIdc++ZqrkvMWHNVqILcpe2upb7DbCVMjrsv5htXrYL7lgaItm
+MpyP20q4ut7ja0YRwPITJyHNacJygAE/TViTL3K1JNXKHXCLWfIHGkVhYzc/9uv7
+0nu5el6crurO57rQFC3T14huEQvouZl9SmHflkBFF7/kQeAJj10bmZWYae8mhdhb
+zaJ2tUcsEgxEaZGrVK4f1NRAwBBY5t5AZMwLcYyIY1F1YZxw2BmrnPqr3GgSCcQ5
+eMo3u9jKXXYJ6Eb3xwNpFXaGFPS1TmTK9y1CU8fXfDOsr7ln
diff --git a/modules/mongodb/src/test/java/org/testcontainers/containers/MongoDBContainerTest.java b/modules/mongodb/src/test/java/org/testcontainers/containers/MongoDBContainerTest.java
index dd59420f937..364bcf0f3fb 100644
--- a/modules/mongodb/src/test/java/org/testcontainers/containers/MongoDBContainerTest.java
+++ b/modules/mongodb/src/test/java/org/testcontainers/containers/MongoDBContainerTest.java
@@ -1,5 +1,8 @@
package org.testcontainers.containers;
+import com.mongodb.BasicDBObject;
+import com.mongodb.ConnectionString;
+import com.mongodb.MongoCommandException;
import com.mongodb.ReadConcern;
import com.mongodb.ReadPreference;
import com.mongodb.TransactionOptions;
@@ -8,12 +11,17 @@
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoDatabase;
import com.mongodb.client.TransactionBody;
import org.bson.Document;
import org.junit.Test;
import org.testcontainers.utility.DockerImageName;
+import java.util.Collections;
+import java.util.Objects;
+
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class MongoDBContainerTest {
@@ -98,7 +106,8 @@ public void shouldTestDatabaseName() {
try (final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.0.10"))) {
mongoDBContainer.start();
final String databaseName = "my-db";
- assertThat(mongoDBContainer.getReplicaSetUrl(databaseName)).endsWith(databaseName);
+ assertThat(databaseName)
+ .isEqualTo(new ConnectionString(mongoDBContainer.getReplicaSetUrl(databaseName)).getDatabase());
}
}
@@ -109,4 +118,73 @@ public void supportsMongoDB_6() {
executeTx(mongoDBContainer);
}
}
+
+ @Test
+ public void shouldTestAuthenticationAccessControl() {
+ final String usernameFullAccess = "my-name";
+ final String passwordFullAccess = "my-pass";
+ try (
+ final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.4"))
+ .withUsername(usernameFullAccess)
+ .withPassword(passwordFullAccess)
+ ) {
+ mongoDBContainer.start();
+ final ConnectionString connectionStringFullAccess = new ConnectionString(
+ mongoDBContainer.getReplicaSetUrl()
+ );
+ try (final MongoClient mongoSyncClientFullAccess = MongoClients.create(connectionStringFullAccess)) {
+ final MongoDatabase adminDatabase = mongoSyncClientFullAccess.getDatabase(
+ MongoDBContainer.DEFAULT_AUTHENTICATION_DATABASE_NAME
+ );
+ final MongoDatabase testDatabaseFullAccess = mongoSyncClientFullAccess.getDatabase(
+ MongoDBContainer.DEFAULT_DATABASE_NAME
+ );
+ final String collectionName = "my-collection";
+ final Document document1 = new Document("abc", 1);
+ testDatabaseFullAccess.getCollection(collectionName).insertOne(document1);
+ final String usernameRestrictedAccess = usernameFullAccess + "-restricted";
+ final String passwordRestrictedAccess = passwordFullAccess + "-restricted";
+ runCommand(
+ adminDatabase,
+ new BasicDBObject("createUser", usernameRestrictedAccess).append("pwd", passwordRestrictedAccess),
+ "read"
+ );
+ try (
+ final MongoClient mongoSyncRestrictedAccess = MongoClients.create(
+ mongoDBContainer.getReplicaSetUrl(
+ MongoDBContainer.ConnectionString
+ .builder()
+ .username(usernameRestrictedAccess)
+ .password(passwordRestrictedAccess)
+ .build()
+ )
+ )
+ ) {
+ final MongoCollection collection = mongoSyncRestrictedAccess
+ .getDatabase(MongoDBContainer.DEFAULT_DATABASE_NAME)
+ .getCollection(collectionName);
+ assertThat(collection.find().first()).isEqualTo(document1);
+ final Document document2 = new Document("abc", 2);
+ assertThatThrownBy(() -> collection.insertOne(document2)).isInstanceOf(MongoCommandException.class);
+ runCommand(adminDatabase, new BasicDBObject("updateUser", usernameRestrictedAccess), "readWrite");
+ collection.insertOne(document2);
+ assertThat(collection.countDocuments()).isEqualTo(2);
+ assertThat(connectionStringFullAccess.getUsername()).isEqualTo(usernameFullAccess);
+ assertThat(new String(Objects.requireNonNull(connectionStringFullAccess.getPassword())))
+ .isEqualTo(passwordFullAccess);
+ }
+ }
+ }
+ }
+
+ private void runCommand(MongoDatabase adminDatabase, BasicDBObject command, String role) {
+ adminDatabase.runCommand(
+ command.append(
+ "roles",
+ Collections.singletonList(
+ new BasicDBObject("role", role).append("db", MongoDBContainer.DEFAULT_DATABASE_NAME)
+ )
+ )
+ );
+ }
}