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) + ) + ) + ); + } }