From 379018023a4ce3598e4f542495e58ca3663fa246 Mon Sep 17 00:00:00 2001 From: Michael Mosmann Date: Wed, 18 Jan 2023 00:16:48 +0100 Subject: [PATCH] auth example --- Howto.md | 226 ++++++++++++++++++ pom.xml | 4 +- .../embed/mongo/commands/MongodArguments.java | 8 + .../embed/mongo/transitions/Mongod.java | 4 +- .../transitions/RunningMongoProcess.java | 201 ++++++++++++++++ .../transitions/RunningMongodProcess.java | 150 +----------- .../transitions/RunningMongosProcess.java | 136 +---------- .../embed/mongo/doc/HowToDocTest.java | 52 ++++ .../mongo/examples/EnableAuthentication.java | 183 ++++++++++++++ .../embed/mongo/examples/MongodAuthTest.java | 200 ++++++++++++++++ .../de/flapdoodle/embed/mongo/doc/Howto.md | 13 + 11 files changed, 897 insertions(+), 280 deletions(-) create mode 100644 src/main/java/de/flapdoodle/embed/mongo/transitions/RunningMongoProcess.java create mode 100644 src/test/java/de/flapdoodle/embed/mongo/examples/EnableAuthentication.java create mode 100644 src/test/java/de/flapdoodle/embed/mongo/examples/MongodAuthTest.java diff --git a/Howto.md b/Howto.md index 1f6ec001..4b1b7a17 100644 --- a/Howto.md +++ b/Howto.md @@ -125,7 +125,39 @@ Mongod mongod = new Mongod() { } }; ... +``` + +```java +// ... +public class FileStreamProcessor implements StreamProcessor { + + private final FileOutputStream outputStream; + + public FileStreamProcessor(File file) throws FileNotFoundException { + outputStream = new FileOutputStream(file); + } + + @Override + public void process(String block) { + try { + outputStream.write(block.getBytes()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void onProcessed() { + try { + outputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} +// ... +// <- ``` #### ... to null device @@ -300,6 +332,200 @@ try (TransitionWalker.ReachedState runningMongoD = transit } ``` + +### User/Roles setup + +```java + +Listener withRunningMongod = EnableAuthentication.of("i-am-admin", "admin-password") + .withEntries( + EnableAuthentication.role("test-db", "test-collection", "can-list-collections") + .withActions("listCollections"), + EnableAuthentication.user("test-db", "read-only", "user-password") + .withRoles("can-list-collections", "read") + ).withRunningMongod(); + +try (TransitionWalker.ReachedState running = Mongod.instance() + .withMongodArguments( + Start.to(MongodArguments.class) + .initializedWith(MongodArguments.defaults().withAuth(true))) + .start(Version.Main.PRODUCTION, withRunningMongod)) { + + try (MongoClient mongo = new MongoClient( + serverAddress(running.current().getServerAddress()), + MongoCredential.createCredential("i-am-admin", "admin", "admin-password".toCharArray()), + MongoClientOptions.builder().build())) { + + MongoDatabase db = mongo.getDatabase("test-db"); + MongoCollection col = db.getCollection("test-collection"); + col.insertOne(new Document("testDoc", new Date())); + } + + try (MongoClient mongo = new MongoClient( + serverAddress(running.current().getServerAddress()), + MongoCredential.createCredential("read-only", "test-db", "user-password".toCharArray()), + MongoClientOptions.builder().build())) { + + MongoDatabase db = mongo.getDatabase("test-db"); + MongoCollection col = db.getCollection("test-collection"); + assertThat(col.countDocuments()).isEqualTo(1L); + + assertThatThrownBy(() -> col.insertOne(new Document("testDoc", new Date()))) + .isInstanceOf(MongoCommandException.class) + .message().contains("not authorized on test-db"); + } +} + +``` + +```java +@Value.Immutable +public abstract class EnableAuthentication { + private static Logger LOGGER= LoggerFactory.getLogger(EnableAuthentication.class); + + @Value.Parameter + protected abstract String adminUser(); + @Value.Parameter + protected abstract String adminPassword(); + + @Value.Default + protected List entries() { + return Collections.emptyList(); + } + + public interface Entry { + + } + + @Value.Immutable + public interface Role extends Entry { + @Value.Parameter + String database(); + @Value.Parameter + String collection(); + @Value.Parameter + String name(); + List actions(); + } + + @Value.Immutable + public interface User extends Entry { + @Value.Parameter + String database(); + @Value.Parameter + String username(); + @Value.Parameter + String password(); + List roles(); + } + + @Value.Auxiliary + public Listener withRunningMongod() { + StateID expectedState = StateID.of(RunningMongodProcess.class); + + return Listener.typedBuilder() + .onStateReached(expectedState, running -> { + final ServerAddress address = serverAddress(running); + + // Create admin user. + try (final MongoClient clientWithoutCredentials = new MongoClient(address)) { + runCommand( + clientWithoutCredentials.getDatabase("admin"), + commandCreateUser(adminUser(), adminPassword(), Arrays.asList("root")) + ); + } + + final MongoCredential credentialAdmin = + MongoCredential.createCredential(adminUser(), "admin", adminPassword().toCharArray()); + + // create roles and users + try (final MongoClient clientAdmin = new MongoClient(address, credentialAdmin, MongoClientOptions.builder().build())) { + entries().forEach(entry -> { + if (entry instanceof Role) { + Role role = (Role) entry; + MongoDatabase db = clientAdmin.getDatabase(role.database()); + runCommand(db, commandCreateRole(role.database(), role.collection(), role.name(), role.actions())); + } + if (entry instanceof User) { + User user = (User) entry; + MongoDatabase db = clientAdmin.getDatabase(user.database()); + runCommand(db, commandCreateUser(user.username(), user.password(), user.roles())); + } + }); + } + + }) + .onStateTearDown(expectedState, running -> { + final ServerAddress address = serverAddress(running); + + final MongoCredential credentialAdmin = + MongoCredential.createCredential(adminUser(), "admin", adminPassword().toCharArray()); + + try (final MongoClient clientAdmin = new MongoClient(address, credentialAdmin, MongoClientOptions.builder().build())) { + try { + // if success there will be no answer, the connection just closes.. + runCommand( + clientAdmin.getDatabase("admin"), + new Document("shutdown", 1) + ); + } catch (MongoSocketReadException mx) { + LOGGER.debug("shutdown completed by closing stream"); + } + } + }) + .build(); + } + + private static void runCommand(MongoDatabase db, Document document) { + Document result = db.runCommand(document); + boolean success = result.get("ok", Double.class) == 1.0d; + Preconditions.checkArgument(success, "runCommand %s failed: %s", document, result); + } + + private static Document commandCreateRole( + String database, + String collection, + String roleName, + List actions + ) { + return new Document("createRole", roleName) + .append("privileges", Collections.singletonList( + new Document("resource", + new Document("db", database) + .append("collection", collection)) + .append("actions", actions) + ) + ).append("roles", Collections.emptyList()); + } + + static Document commandCreateUser( + final String username, + final String password, + final List roles + ) { + return new Document("createUser", username) + .append("pwd", password) + .append("roles", roles); + } + + private static ServerAddress serverAddress(RunningMongodProcess running) { + de.flapdoodle.embed.mongo.commands.ServerAddress serverAddress = running.getServerAddress(); + return new ServerAddress(serverAddress.getHost(), serverAddress.getPort()); + } + + public static ImmutableRole role(String database, String collection, String name) { + return ImmutableRole.of(database, collection, name); + } + + public static ImmutableUser user(String database, String username, String password) { + return ImmutableUser.of(database, username, password); + } + + public static ImmutableEnableAuthentication of(String adminUser, String adminPassword) { + return ImmutableEnableAuthentication.of(adminUser,adminPassword); + } +} +``` ---- diff --git a/pom.xml b/pom.xml index 269412b6..a0f8ff5a 100644 --- a/pom.xml +++ b/pom.xml @@ -443,7 +443,7 @@ org.assertj assertj-core - 3.24.1 + 3.24.2 test @@ -489,7 +489,7 @@ de.flapdoodle.embed de.flapdoodle.embed.process - 4.3.5 + 4.4.2 de.flapdoodle.embed diff --git a/src/main/java/de/flapdoodle/embed/mongo/commands/MongodArguments.java b/src/main/java/de/flapdoodle/embed/mongo/commands/MongodArguments.java index a6fae65f..f4e110ef 100644 --- a/src/main/java/de/flapdoodle/embed/mongo/commands/MongodArguments.java +++ b/src/main/java/de/flapdoodle/embed/mongo/commands/MongodArguments.java @@ -137,6 +137,14 @@ private static List getCommandLine( config.params().forEach((key, val) -> builder.add("--setParameter", format("%s=%s", key, val))); config.args().forEach(builder::add); + if (config.auth()) { + LOGGER.info( + "\n---------------------------------------\n" + + "hint: auth==true starts mongod with authorization enabled, which, if started from scratch must fail, as a " + + "connect is only possible for known user.\n" + + "---------------------------------------\n"); + } + builder.add(config.auth() ? "--auth" : "--noauth"); builder.addIf(!version.enabled(Feature.DISABLE_USE_PREALLOC) && config.useNoPrealloc(), "--noprealloc"); diff --git a/src/main/java/de/flapdoodle/embed/mongo/transitions/Mongod.java b/src/main/java/de/flapdoodle/embed/mongo/transitions/Mongod.java index b9041ea6..c1933de6 100644 --- a/src/main/java/de/flapdoodle/embed/mongo/transitions/Mongod.java +++ b/src/main/java/de/flapdoodle/embed/mongo/transitions/Mongod.java @@ -84,10 +84,10 @@ public Transitions transitions(de.flapdoodle.embed.process.distribution.Version } @Value.Auxiliary - public TransitionWalker.ReachedState start(Version version) { + public TransitionWalker.ReachedState start(Version version, Listener... listener) { return transitions(version) .walker() - .initState(StateID.of(RunningMongodProcess.class)); + .initState(StateID.of(RunningMongodProcess.class), listener); } public static ImmutableMongod instance() { diff --git a/src/main/java/de/flapdoodle/embed/mongo/transitions/RunningMongoProcess.java b/src/main/java/de/flapdoodle/embed/mongo/transitions/RunningMongoProcess.java new file mode 100644 index 00000000..b838e471 --- /dev/null +++ b/src/main/java/de/flapdoodle/embed/mongo/transitions/RunningMongoProcess.java @@ -0,0 +1,201 @@ +/** + * Copyright (C) 2011 + * Michael Mosmann + * Martin Jöhren + * + * with contributions from + * konstantin-ba@github,Archimedes Trajano (trajano@github) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.flapdoodle.embed.mongo.transitions; + +import de.flapdoodle.embed.mongo.commands.ServerAddress; +import de.flapdoodle.embed.mongo.config.Net; +import de.flapdoodle.embed.mongo.runtime.Mongod; +import de.flapdoodle.embed.process.config.SupportConfig; +import de.flapdoodle.embed.process.io.*; +import de.flapdoodle.embed.process.runtime.ProcessControl; +import de.flapdoodle.embed.process.runtime.Processes; +import de.flapdoodle.embed.process.types.RunningProcessFactory; +import de.flapdoodle.embed.process.types.RunningProcessImpl; +import de.flapdoodle.os.Platform; +import de.flapdoodle.types.Try; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetAddress; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; + +public abstract class RunningMongoProcess extends RunningProcessImpl { + + private static Logger LOGGER= LoggerFactory.getLogger(RunningMongodProcess.class); + + private final String commandName; + private final SupportConfig supportConfig; + private final Platform platform; + private final StreamProcessor commandOutput; + private final int mongoProcessId; + private final InetAddress serverAddress; + private final int port; + + protected RunningMongoProcess( + String commandName, + ProcessControl process, + Path pidFile, + long timeout, + Runnable onStop, + SupportConfig supportConfig, + Platform platform, + Net net, + StreamProcessor commandOutput, + int mongoProcessId +// boolean withAuthEnabled + ) { + super(process, pidFile, timeout, onStop); + this.commandName = commandName; + this.supportConfig = supportConfig; + this.platform = platform; + this.commandOutput = commandOutput; + this.mongoProcessId = mongoProcessId; + this.serverAddress = Try.get(net::getServerAddress); + this.port = net.getPort(); + } + + public ServerAddress getServerAddress() { + return ServerAddress.of(serverAddress, port); + } + + @Override + public int stop() { + try { + stopInternal(); + } finally { + return super.stop(); + } + } + + // @Override + private void stopInternal() { + if (isAlive()) { + LOGGER.debug("try to stop "+commandName); + if (!sendStopToMongoInstance()) { + LOGGER.warn("could not stop "+commandName+" with db command, try next"); + if (!sendKillToProcess()) { + LOGGER.warn("could not stop "+commandName+", try next"); + if (!sendTermToProcess()) { + LOGGER.warn("could not stop "+commandName+", try next"); + if (!tryKillToProcess()) { + LOGGER.warn("could not stop "+commandName+" the second time, try one last thing"); + } + } + } + } + } + } + + private long getProcessId() { + return mongoProcessId; + } + + protected boolean sendKillToProcess() { + return getProcessId() > 0 && Processes.killProcess(supportConfig, platform, + StreamToLineProcessor.wrap(commandOutput), getProcessId()); + } + + protected boolean sendTermToProcess() { + return getProcessId() > 0 && Processes.termProcess(supportConfig, platform, + StreamToLineProcessor.wrap(commandOutput), getProcessId()); + } + + protected boolean tryKillToProcess() { + return getProcessId() > 0 && Processes.tryKillProcess(supportConfig, platform, + StreamToLineProcessor.wrap(commandOutput), getProcessId()); + } + + protected final boolean sendStopToMongoInstance() { + return Mongod.sendShutdownLegacy(serverAddress, port) + || Mongod.sendShutdown(serverAddress, port); + } + + interface InstanceFactory { + T create(ProcessControl process, Path pidFile, long timeout, Runnable closeAllOutputs, SupportConfig supportConfig, Platform platform, Net net, StreamProcessor commands, int pid); + } + + static RunningProcessFactory factory(InstanceFactory instanceFactory, long startupTimeout, SupportConfig supportConfig, Platform platform, Net net) { + return (process, processOutput, pidFile, timeout) -> { + +// LogWatchStreamProcessor logWatch = new LogWatchStreamProcessor(successMessage(), knownFailureMessages(), +// StreamToLineProcessor.wrap(processOutput.output())); + LOGGER.trace("setup logWatch"); + SuccessMessageLineListener logWatch = SuccessMessageLineListener.of(successMessage(), knownFailureMessages(), "error"); + + LOGGER.trace("connect io"); + ReaderProcessor output = Processors.connect(process.getReader(), new ListeningStreamProcessor(StreamToLineProcessor.wrap(processOutput.output()), logWatch::inspect)); + ReaderProcessor error = Processors.connect(process.getError(), StreamToLineProcessor.wrap(processOutput.error())); + Runnable closeAllOutputs = () -> { + LOGGER.trace("ReaderProcessor.abortAll"); + ReaderProcessor.abortAll(output, error); + LOGGER.trace("ReaderProcessor.abortAll done"); + }; + + LOGGER.trace("waitForResult"); + logWatch.waitForResult(startupTimeout); + LOGGER.trace("check if successMessageFound"); + if (logWatch.successMessageFound()) { + LOGGER.trace("get processId"); + int pid = Mongod.getMongodProcessId(logWatch.allLines(), -1); + LOGGER.trace("return RunningMongodProcess"); + return instanceFactory.create(process, pidFile, timeout, closeAllOutputs, supportConfig, platform, net, processOutput.commands(), pid); + + } else { + String failureFound = logWatch.errorMessage().isPresent() + ? logWatch.errorMessage().get() + : "\n" + + "----------------------\n" + + "Hmm.. no failure message.. \n" + + "...the cause must be somewhere in the process output\n" + + "----------------------\n" + + ""+logWatch.allLines(); + + return Try.supplier(() -> { + throw new RuntimeException("Could not start process: "+failureFound); + }) + .andFinally(() -> { + process.stop(timeout); + }) + .andFinally(closeAllOutputs) + .get(); + } + }; + } + + private static List successMessage() { + // old: waiting for connections on port + // since 4.4.5: Waiting for connections + return Arrays.asList("aiting for connections"); + } + + private static List knownFailureMessages() { + return Arrays.asList( + "(?failed errno)", + "ERROR:(?.*)", + "(?error command line)", + "(?Address already in use)", + "(?error while loading shared libraries:.*)" + ); + } + +} diff --git a/src/main/java/de/flapdoodle/embed/mongo/transitions/RunningMongodProcess.java b/src/main/java/de/flapdoodle/embed/mongo/transitions/RunningMongodProcess.java index 34723560..a5ac29f9 100644 --- a/src/main/java/de/flapdoodle/embed/mongo/transitions/RunningMongodProcess.java +++ b/src/main/java/de/flapdoodle/embed/mongo/transitions/RunningMongodProcess.java @@ -20,35 +20,17 @@ */ package de.flapdoodle.embed.mongo.transitions; -import de.flapdoodle.embed.mongo.commands.ServerAddress; import de.flapdoodle.embed.mongo.config.Net; -import de.flapdoodle.embed.mongo.runtime.Mongod; import de.flapdoodle.embed.process.config.SupportConfig; -import de.flapdoodle.embed.process.io.*; +import de.flapdoodle.embed.process.io.StreamProcessor; import de.flapdoodle.embed.process.runtime.ProcessControl; -import de.flapdoodle.embed.process.runtime.Processes; import de.flapdoodle.embed.process.types.RunningProcessFactory; -import de.flapdoodle.embed.process.types.RunningProcessImpl; import de.flapdoodle.os.Platform; -import de.flapdoodle.types.Try; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.net.UnknownHostException; import java.nio.file.Path; -import java.util.Arrays; -import java.util.List; -public class RunningMongodProcess extends RunningProcessImpl { +public class RunningMongodProcess extends RunningMongoProcess { - private static Logger LOGGER= LoggerFactory.getLogger(RunningMongodProcess.class); - - private final SupportConfig supportConfig; - private final Platform platform; - private final Net net; - private final StreamProcessor commandOutput; - private final int mongodProcessId; - public RunningMongodProcess( ProcessControl process, Path pidFile, @@ -59,134 +41,12 @@ public RunningMongodProcess( Net net, StreamProcessor commandOutput, int mongodProcessId +// boolean withAuthEnabled ) { - super(process, pidFile, timeout, onStop); - this.supportConfig = supportConfig; - this.platform = platform; - this.commandOutput = commandOutput; - this.net = net; - this.mongodProcessId = mongodProcessId; - } - - public ServerAddress getServerAddress() throws UnknownHostException { - return ServerAddress.of(net.getServerAddress(), net.getPort()); - } - - @Override - public int stop() { - try { - stopInternal(); - } finally { - return super.stop(); - } - } - - // @Override - private void stopInternal() { - LOGGER.debug("try to stop mongod"); - if (!sendStopToMongoInstance()) { - LOGGER.warn("could not stop mongod with db command, try next"); - if (!sendKillToProcess()) { - LOGGER.warn("could not stop mongod, try next"); - if (!sendTermToProcess()) { - LOGGER.warn("could not stop mongod, try next"); - if (!tryKillToProcess()) { - LOGGER.warn("could not stop mongod the second time, try one last thing"); - } - } - } - } - } - - private long getProcessId() { - return mongodProcessId; - } - - protected boolean sendKillToProcess() { - return getProcessId() > 0 && Processes.killProcess(supportConfig, platform, - StreamToLineProcessor.wrap(commandOutput), getProcessId()); - } - - protected boolean sendTermToProcess() { - return getProcessId() > 0 && Processes.termProcess(supportConfig, platform, - StreamToLineProcessor.wrap(commandOutput), getProcessId()); - } - - protected boolean tryKillToProcess() { - return getProcessId() > 0 && Processes.tryKillProcess(supportConfig, platform, - StreamToLineProcessor.wrap(commandOutput), getProcessId()); - } - - protected final boolean sendStopToMongoInstance() { - try { - return Mongod.sendShutdownLegacy(net.getServerAddress(), net.getPort()) - || Mongod.sendShutdown(net.getServerAddress(), net.getPort()); - } catch (UnknownHostException e) { - LOGGER.error("sendStop", e); - } - return false; + super("mongod", process, pidFile, timeout, onStop, supportConfig, platform, net, commandOutput, mongodProcessId); } public static RunningProcessFactory factory(long startupTimeout, SupportConfig supportConfig, Platform platform, Net net) { - return (process, processOutput, pidFile, timeout) -> { - -// LogWatchStreamProcessor logWatch = new LogWatchStreamProcessor(successMessage(), knownFailureMessages(), -// StreamToLineProcessor.wrap(processOutput.output())); - LOGGER.trace("setup logWatch"); - SuccessMessageLineListener logWatch = SuccessMessageLineListener.of(successMessage(), knownFailureMessages(), "error"); - - LOGGER.trace("connect io"); - ReaderProcessor output = Processors.connect(process.getReader(), new ListeningStreamProcessor(StreamToLineProcessor.wrap(processOutput.output()), logWatch::inspect)); - ReaderProcessor error = Processors.connect(process.getError(), StreamToLineProcessor.wrap(processOutput.error())); - Runnable closeAllOutputs = () -> { - LOGGER.trace("ReaderProcessor.abortAll"); - ReaderProcessor.abortAll(output, error); - LOGGER.trace("ReaderProcessor.abortAll done"); - }; - - LOGGER.trace("waitForResult"); - logWatch.waitForResult(startupTimeout); - LOGGER.trace("check if successMessageFound"); - if (logWatch.successMessageFound()) { - LOGGER.trace("getMongodProcessId"); - int pid = Mongod.getMongodProcessId(logWatch.allLines(), -1); - LOGGER.trace("return RunningMongodProcess"); - return new RunningMongodProcess(process, pidFile, timeout, closeAllOutputs, supportConfig, platform, net, processOutput.commands(), pid); - - } else { - String failureFound = logWatch.errorMessage().isPresent() - ? logWatch.errorMessage().get() - : "\n" + - "----------------------\n" + - "Hmm.. no failure message.. \n" + - "...the cause must be somewhere in the process output\n" + - "----------------------\n" + - ""+logWatch.allLines(); - - return Try.supplier(() -> { - throw new RuntimeException("Could not start process: "+failureFound); - }) - .andFinally(() -> process.stop(timeout)) - .andFinally(closeAllOutputs) - .get(); - } - }; - } - - private static List successMessage() { - // old: waiting for connections on port - // since 4.4.5: Waiting for connections - return Arrays.asList("aiting for connections"); + return RunningMongoProcess.factory(RunningMongodProcess::new, startupTimeout, supportConfig, platform, net); } - - private static List knownFailureMessages() { - return Arrays.asList( - "(?failed errno)", - "ERROR:(?.*)", - "(?error command line)", - "(?Address already in use)", - "(?error while loading shared libraries:.*)" - ); - } - } diff --git a/src/main/java/de/flapdoodle/embed/mongo/transitions/RunningMongosProcess.java b/src/main/java/de/flapdoodle/embed/mongo/transitions/RunningMongosProcess.java index ba3089c5..d0e0f076 100644 --- a/src/main/java/de/flapdoodle/embed/mongo/transitions/RunningMongosProcess.java +++ b/src/main/java/de/flapdoodle/embed/mongo/transitions/RunningMongosProcess.java @@ -20,34 +20,16 @@ */ package de.flapdoodle.embed.mongo.transitions; -import de.flapdoodle.embed.mongo.commands.ServerAddress; import de.flapdoodle.embed.mongo.config.Net; -import de.flapdoodle.embed.mongo.runtime.Mongod; import de.flapdoodle.embed.process.config.SupportConfig; -import de.flapdoodle.embed.process.io.*; +import de.flapdoodle.embed.process.io.StreamProcessor; import de.flapdoodle.embed.process.runtime.ProcessControl; -import de.flapdoodle.embed.process.runtime.Processes; import de.flapdoodle.embed.process.types.RunningProcessFactory; -import de.flapdoodle.embed.process.types.RunningProcessImpl; import de.flapdoodle.os.Platform; -import de.flapdoodle.types.Try; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.net.UnknownHostException; import java.nio.file.Path; -import java.util.HashSet; -import java.util.Set; -public class RunningMongosProcess extends RunningProcessImpl { - - private static Logger LOGGER= LoggerFactory.getLogger(RunningMongosProcess.class); - - private final SupportConfig supportConfig; - private final Platform platform; - private final Net net; - private final StreamProcessor commandOutput; - private final int mongodProcessId; +public class RunningMongosProcess extends RunningMongoProcess { public RunningMongosProcess( ProcessControl process, @@ -59,120 +41,12 @@ public RunningMongosProcess( Net net, StreamProcessor commandOutput, int mongodProcessId +// boolean withAuthEnabled ) { - super(process, pidFile, timeout, onStop); - this.supportConfig = supportConfig; - this.platform = platform; - this.commandOutput = commandOutput; - this.net = net; - this.mongodProcessId = mongodProcessId; - } - - public ServerAddress getServerAddress() throws UnknownHostException { - return ServerAddress.of(net.getServerAddress(), net.getPort()); - } - - @Override - public int stop() { - try { - stopInternal(); - } finally { - return super.stop(); - } - } - - // @Override - private void stopInternal() { - LOGGER.debug("try to stop mongod"); - if (!sendStopToMongoInstance()) { - LOGGER.warn("could not stop mongod with db command, try next"); - if (!sendKillToProcess()) { - LOGGER.warn("could not stop mongod, try next"); - if (!sendTermToProcess()) { - LOGGER.warn("could not stop mongod, try next"); - if (!tryKillToProcess()) { - LOGGER.warn("could not stop mongod the second time, try one last thing"); - } - } - } - } - } - - private long getProcessId() { - return mongodProcessId; - } - - protected boolean sendKillToProcess() { - return getProcessId() > 0 && Processes.killProcess(supportConfig, platform, - StreamToLineProcessor.wrap(commandOutput), getProcessId()); - } - - protected boolean sendTermToProcess() { - return getProcessId() > 0 && Processes.termProcess(supportConfig, platform, - StreamToLineProcessor.wrap(commandOutput), getProcessId()); - } - - protected boolean tryKillToProcess() { - return getProcessId() > 0 && Processes.tryKillProcess(supportConfig, platform, - StreamToLineProcessor.wrap(commandOutput), getProcessId()); - } - - protected final boolean sendStopToMongoInstance() { - try { - return Mongod.sendShutdownLegacy(net.getServerAddress(), net.getPort()) - || Mongod.sendShutdown(net.getServerAddress(), net.getPort()); - } catch (UnknownHostException e) { - LOGGER.error("sendStop", e); - } - return false; + super("mongos", process, pidFile, timeout, onStop, supportConfig, platform, net, commandOutput, mongodProcessId); } public static RunningProcessFactory factory(long startupTimeout, SupportConfig supportConfig, Platform platform, Net net) { - return (process, processOutput, pidFile, timeout) -> { - - LogWatchStreamProcessor logWatch = new LogWatchStreamProcessor(successMessage(), knownFailureMessages(), - StreamToLineProcessor.wrap(processOutput.output())); - ReaderProcessor output = Processors.connect(process.getReader(), logWatch); - ReaderProcessor error = Processors.connect(process.getError(), StreamToLineProcessor.wrap(processOutput.error())); - Runnable closeAllOutputs = () -> ReaderProcessor.abortAll(output, error); - - logWatch.waitForResult(startupTimeout); - if (logWatch.isInitWithSuccess()) { - int pid = Mongod.getMongodProcessId(logWatch.getOutput(), -1); - return new RunningMongosProcess(process, pidFile, timeout, closeAllOutputs, supportConfig, platform, net, processOutput.commands(), pid); - - } else { - String failureFound = logWatch.getFailureFound() != null - ? logWatch.getFailureFound() - : "\n" + - "----------------------\n" + - "Hmm.. no failure message.. \n" + - "...the cause must be somewhere in the process output\n" + - "----------------------\n" + - ""+logWatch.getOutput(); - - return Try.supplier(() -> { - throw new RuntimeException("Could not start process: "+failureFound); - }) - .andFinally(() -> process.stop(timeout)) - .andFinally(closeAllOutputs) - .get(); - } - }; - } - - private static String successMessage() { - // old: waiting for connections on port - // since 4.4.5: Waiting for connections - return "aiting for connections"; + return RunningMongoProcess.factory(RunningMongosProcess::new, startupTimeout, supportConfig, platform, net); } - - private static Set knownFailureMessages() { - HashSet ret = new HashSet<>(); - ret.add("failed errno"); - ret.add("ERROR:"); - ret.add("error command line"); - return ret; - } - } diff --git a/src/test/java/de/flapdoodle/embed/mongo/doc/HowToDocTest.java b/src/test/java/de/flapdoodle/embed/mongo/doc/HowToDocTest.java index cb8cf6ae..6224135c 100644 --- a/src/test/java/de/flapdoodle/embed/mongo/doc/HowToDocTest.java +++ b/src/test/java/de/flapdoodle/embed/mongo/doc/HowToDocTest.java @@ -22,6 +22,9 @@ import com.google.common.io.Resources; import com.mongodb.MongoClient; +import com.mongodb.MongoClientOptions; +import com.mongodb.MongoCommandException; +import com.mongodb.MongoCredential; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; import de.flapdoodle.embed.mongo.commands.MongoImportArguments; @@ -31,6 +34,7 @@ import de.flapdoodle.embed.mongo.config.Storage; import de.flapdoodle.embed.mongo.distribution.IFeatureAwareVersion; import de.flapdoodle.embed.mongo.distribution.Version; +import de.flapdoodle.embed.mongo.examples.EnableAuthentication; import de.flapdoodle.embed.mongo.examples.FileStreamProcessor; import de.flapdoodle.embed.mongo.transitions.*; import de.flapdoodle.embed.mongo.types.DatabaseDir; @@ -409,6 +413,54 @@ public void importJsonIntoMongoDB() { recording.end(); } + @Test + public void setupUserAndRoles() { + recording.include(EnableAuthentication.class, Includes.WithoutImports, Includes.WithoutPackage, Includes.Trim); + recording.begin(); + + Listener withRunningMongod = EnableAuthentication.of("i-am-admin", "admin-password") + .withEntries( + EnableAuthentication.role("test-db", "test-collection", "can-list-collections") + .withActions("listCollections"), + EnableAuthentication.user("test-db", "read-only", "user-password") + .withRoles("can-list-collections", "read") + ).withRunningMongod(); + + try (TransitionWalker.ReachedState running = Mongod.instance() + .withMongodArguments( + Start.to(MongodArguments.class) + .initializedWith(MongodArguments.defaults().withAuth(true))) + .start(Version.Main.PRODUCTION, withRunningMongod)) { + + try (MongoClient mongo = new MongoClient( + serverAddress(running.current().getServerAddress()), + MongoCredential.createCredential("i-am-admin", "admin", "admin-password".toCharArray()), + MongoClientOptions.builder().build())) { + + MongoDatabase db = mongo.getDatabase("test-db"); + MongoCollection col = db.getCollection("test-collection"); + col.insertOne(new Document("testDoc", new Date())); + } + + try (MongoClient mongo = new MongoClient( + serverAddress(running.current().getServerAddress()), + MongoCredential.createCredential("read-only", "test-db", "user-password".toCharArray()), + MongoClientOptions.builder().build())) { + + MongoDatabase db = mongo.getDatabase("test-db"); + MongoCollection col = db.getCollection("test-collection"); + assertThat(col.countDocuments()).isEqualTo(1L); + + assertThatThrownBy(() -> col.insertOne(new Document("testDoc", new Date()))) + .isInstanceOf(MongoCommandException.class) + .message().contains("not authorized on test-db"); + } + } + + recording.end(); + } + + private static void assertRunningMongoDB(TransitionWalker.ReachedState running) throws UnknownHostException { try (MongoClient mongo = new MongoClient(serverAddress(running.current().getServerAddress()))) { MongoDatabase db = mongo.getDatabase("test"); diff --git a/src/test/java/de/flapdoodle/embed/mongo/examples/EnableAuthentication.java b/src/test/java/de/flapdoodle/embed/mongo/examples/EnableAuthentication.java new file mode 100644 index 00000000..ada61a0d --- /dev/null +++ b/src/test/java/de/flapdoodle/embed/mongo/examples/EnableAuthentication.java @@ -0,0 +1,183 @@ +/** + * Copyright (C) 2011 + * Michael Mosmann + * Martin Jöhren + * + * with contributions from + * konstantin-ba@github,Archimedes Trajano (trajano@github) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.flapdoodle.embed.mongo.examples; + +import com.mongodb.*; +import com.mongodb.client.MongoDatabase; +import de.flapdoodle.checks.Preconditions; +import de.flapdoodle.embed.mongo.transitions.RunningMongodProcess; +import de.flapdoodle.reverse.Listener; +import de.flapdoodle.reverse.StateID; +import org.bson.Document; +import org.immutables.value.Value; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +@Value.Immutable +public abstract class EnableAuthentication { + private static Logger LOGGER= LoggerFactory.getLogger(EnableAuthentication.class); + + @Value.Parameter + protected abstract String adminUser(); + @Value.Parameter + protected abstract String adminPassword(); + + @Value.Default + protected List entries() { + return Collections.emptyList(); + } + + public interface Entry { + + } + + @Value.Immutable + public interface Role extends Entry { + @Value.Parameter + String database(); + @Value.Parameter + String collection(); + @Value.Parameter + String name(); + List actions(); + } + + @Value.Immutable + public interface User extends Entry { + @Value.Parameter + String database(); + @Value.Parameter + String username(); + @Value.Parameter + String password(); + List roles(); + } + + @Value.Auxiliary + public Listener withRunningMongod() { + StateID expectedState = StateID.of(RunningMongodProcess.class); + + return Listener.typedBuilder() + .onStateReached(expectedState, running -> { + final ServerAddress address = serverAddress(running); + + // Create admin user. + try (final MongoClient clientWithoutCredentials = new MongoClient(address)) { + runCommand( + clientWithoutCredentials.getDatabase("admin"), + commandCreateUser(adminUser(), adminPassword(), Arrays.asList("root")) + ); + } + + final MongoCredential credentialAdmin = + MongoCredential.createCredential(adminUser(), "admin", adminPassword().toCharArray()); + + // create roles and users + try (final MongoClient clientAdmin = new MongoClient(address, credentialAdmin, MongoClientOptions.builder().build())) { + entries().forEach(entry -> { + if (entry instanceof Role) { + Role role = (Role) entry; + MongoDatabase db = clientAdmin.getDatabase(role.database()); + runCommand(db, commandCreateRole(role.database(), role.collection(), role.name(), role.actions())); + } + if (entry instanceof User) { + User user = (User) entry; + MongoDatabase db = clientAdmin.getDatabase(user.database()); + runCommand(db, commandCreateUser(user.username(), user.password(), user.roles())); + } + }); + } + + }) + .onStateTearDown(expectedState, running -> { + final ServerAddress address = serverAddress(running); + + final MongoCredential credentialAdmin = + MongoCredential.createCredential(adminUser(), "admin", adminPassword().toCharArray()); + + try (final MongoClient clientAdmin = new MongoClient(address, credentialAdmin, MongoClientOptions.builder().build())) { + try { + // if success there will be no answer, the connection just closes.. + runCommand( + clientAdmin.getDatabase("admin"), + new Document("shutdown", 1) + ); + } catch (MongoSocketReadException mx) { + LOGGER.debug("shutdown completed by closing stream"); + } + } + }) + .build(); + } + + private static void runCommand(MongoDatabase db, Document document) { + Document result = db.runCommand(document); + boolean success = result.get("ok", Double.class) == 1.0d; + Preconditions.checkArgument(success, "runCommand %s failed: %s", document, result); + } + + private static Document commandCreateRole( + String database, + String collection, + String roleName, + List actions + ) { + return new Document("createRole", roleName) + .append("privileges", Collections.singletonList( + new Document("resource", + new Document("db", database) + .append("collection", collection)) + .append("actions", actions) + ) + ).append("roles", Collections.emptyList()); + } + + static Document commandCreateUser( + final String username, + final String password, + final List roles + ) { + return new Document("createUser", username) + .append("pwd", password) + .append("roles", roles); + } + + private static ServerAddress serverAddress(RunningMongodProcess running) { + de.flapdoodle.embed.mongo.commands.ServerAddress serverAddress = running.getServerAddress(); + return new ServerAddress(serverAddress.getHost(), serverAddress.getPort()); + } + + public static ImmutableRole role(String database, String collection, String name) { + return ImmutableRole.of(database, collection, name); + } + + public static ImmutableUser user(String database, String username, String password) { + return ImmutableUser.of(database, username, password); + } + + public static ImmutableEnableAuthentication of(String adminUser, String adminPassword) { + return ImmutableEnableAuthentication.of(adminUser,adminPassword); + } +} diff --git a/src/test/java/de/flapdoodle/embed/mongo/examples/MongodAuthTest.java b/src/test/java/de/flapdoodle/embed/mongo/examples/MongodAuthTest.java new file mode 100644 index 00000000..62023dac --- /dev/null +++ b/src/test/java/de/flapdoodle/embed/mongo/examples/MongodAuthTest.java @@ -0,0 +1,200 @@ +/** + * Copyright (C) 2011 + * Michael Mosmann + * Martin Jöhren + * + * with contributions from + * konstantin-ba@github,Archimedes Trajano (trajano@github) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.flapdoodle.embed.mongo.examples; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.mongodb.MongoClient; +import com.mongodb.MongoClientOptions; +import com.mongodb.MongoCredential; +import com.mongodb.ServerAddress; +import com.mongodb.client.MongoDatabase; +import de.flapdoodle.embed.mongo.commands.MongodArguments; +import de.flapdoodle.embed.mongo.config.Net; +import de.flapdoodle.embed.mongo.distribution.Version; +import de.flapdoodle.embed.mongo.transitions.Mongod; +import de.flapdoodle.embed.mongo.transitions.RunningMongodProcess; +import de.flapdoodle.reverse.Listener; +import de.flapdoodle.reverse.TransitionWalker; +import de.flapdoodle.reverse.transitions.Start; +import org.bson.Document; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MongodAuthTest { + private static final int PORT = 27018; + private static final String USERNAME_ADMIN = "admin-user"; + private static final String PASSWORD_ADMIN = "admin-password"; + private static final String USERNAME_NORMAL_USER = "test-db-user"; + private static final String PASSWORD_NORMAL_USER = "test-db-user-password"; + private static final String DB_ADMIN = "admin"; + private static final String DB_TEST = "test-db"; + private static final String COLL_TEST = "test-coll"; + + @Test + @Disabled("with --auth shutdown on tearDown does not work") + public void noRole() { + try (final TransitionWalker.ReachedState running = startMongod(true)) { + final ServerAddress address = getServerAddress(running); + + try (final MongoClient clientWithoutCredentials = new MongoClient(address)) { + // do nothing + } + } + } + + @Test + public void customRole() { + String roleName = "listColls"; + Listener withRunningMongod = EnableAuthentication.of(USERNAME_ADMIN, PASSWORD_ADMIN) + .withEntries( + EnableAuthentication.role(DB_TEST, COLL_TEST, roleName).withActions("listCollections"), + EnableAuthentication.user(DB_TEST, USERNAME_NORMAL_USER, PASSWORD_NORMAL_USER).withRoles(roleName, "readWrite") + ) + .withRunningMongod(); + + + try (final TransitionWalker.ReachedState running = startMongod(true, withRunningMongod)) { + final ServerAddress address = getServerAddress(running); + + final MongoCredential credentialAdmin = + MongoCredential.createCredential(USERNAME_ADMIN, DB_ADMIN, PASSWORD_ADMIN.toCharArray()); + + try (final MongoClient clientAdmin = new MongoClient(address, credentialAdmin, MongoClientOptions.builder().build())) { + final MongoDatabase db = clientAdmin.getDatabase(DB_TEST); + db.getCollection(COLL_TEST) + .insertOne(new Document(ImmutableMap.of("key", "value"))); + } + + final MongoCredential credentialNormalUser = + MongoCredential.createCredential(USERNAME_NORMAL_USER, DB_TEST, PASSWORD_NORMAL_USER.toCharArray()); + + try (final MongoClient clientNormalUser = + new MongoClient(address, credentialNormalUser, MongoClientOptions.builder().build())) { + final ArrayList actual = clientNormalUser.getDatabase(DB_TEST).listCollectionNames().into(new ArrayList<>()); + assertThat(actual).containsExactly(COLL_TEST); + } + } + } + + @Test + @Disabled("readAnyDatabase is not assignable") + public void readAnyDatabaseRole() { + Listener withRunningMongod = EnableAuthentication.of(USERNAME_ADMIN, PASSWORD_ADMIN) + .withEntries( + EnableAuthentication.user(DB_TEST, USERNAME_NORMAL_USER, PASSWORD_NORMAL_USER).withRoles("readAnyDatabase") + ) + .withRunningMongod(); + + try (final TransitionWalker.ReachedState running = startMongod(withRunningMongod)) { + final ServerAddress address = getServerAddress(running); +// try (final MongoClient clientWithoutCredentials = new MongoClient(address)) { +// // Create admin user. +// clientWithoutCredentials.getDatabase(DB_ADMIN) +// .runCommand(commandCreateUser(USERNAME_ADMIN, PASSWORD_ADMIN, "root")); +// } + + final MongoCredential credentialAdmin = + MongoCredential.createCredential(USERNAME_ADMIN, DB_ADMIN, PASSWORD_ADMIN.toCharArray()); + + try (final MongoClient clientAdmin = new MongoClient(address, credentialAdmin, MongoClientOptions.builder().build())) { + final MongoDatabase db = clientAdmin.getDatabase(DB_TEST); +// // Create normal user and grant them the builtin "readAnyDatabase" role. +// // FIXME This unexpectedly fails with "No role named readAnyDatabase@test-db". +// db.runCommand(commandCreateUser(USERNAME_NORMAL_USER, PASSWORD_NORMAL_USER, "readAnyDatabase")); + // Create collection. + db.getCollection(COLL_TEST).insertOne(new Document(ImmutableMap.of("key", "value"))); + } + + final MongoCredential credentialNormalUser = + MongoCredential.createCredential(USERNAME_NORMAL_USER, DB_TEST, PASSWORD_NORMAL_USER.toCharArray()); + + try (final MongoClient clientNormalUser = + new MongoClient(address, credentialNormalUser, MongoClientOptions.builder().build())) { + final ArrayList actual = clientNormalUser.getDatabase(DB_TEST).listCollectionNames().into(new ArrayList<>()); + assertThat(actual) + .containsExactly(COLL_TEST); + } + } + } + + @Test + public void readRole() { + Listener withRunningMongod = EnableAuthentication.of(USERNAME_ADMIN, PASSWORD_ADMIN) + .withEntries( + EnableAuthentication.user(DB_TEST, USERNAME_NORMAL_USER, PASSWORD_NORMAL_USER).withRoles("read") + ) + .withRunningMongod(); + + try (final TransitionWalker.ReachedState running = startMongod(withRunningMongod)) { + final ServerAddress address = getServerAddress(running); + + final MongoCredential credentialAdmin = + MongoCredential.createCredential(USERNAME_ADMIN, DB_ADMIN, PASSWORD_ADMIN.toCharArray()); + + try (final MongoClient clientAdmin = new MongoClient(address, credentialAdmin, MongoClientOptions.builder().build())) { + final MongoDatabase db = clientAdmin.getDatabase(DB_TEST); + db.getCollection(COLL_TEST).insertOne(new Document(ImmutableMap.of("key", "value"))); + } + + final MongoCredential credentialNormalUser = + MongoCredential.createCredential(USERNAME_NORMAL_USER, DB_TEST, PASSWORD_NORMAL_USER.toCharArray()); + + try (final MongoClient clientNormalUser = + new MongoClient(address, credentialNormalUser, MongoClientOptions.builder().build())) { + final List expected = Lists.newArrayList(COLL_TEST); + final ArrayList actual = clientNormalUser.getDatabase(DB_TEST).listCollectionNames().into(new ArrayList<>()); + Assertions.assertIterableEquals(expected, actual); + } + } + } + + private static TransitionWalker.ReachedState startMongod(Listener... listener) { + return startMongod(false, listener); + } + + private static TransitionWalker.ReachedState startMongod(boolean withAuth, Listener... listener) { + return Mongod.builder() + .net(Start.to(Net.class) + .initializedWith(Net.defaults().withPort( PORT)) + ) + .mongodArguments(Start.to(MongodArguments.class) + .initializedWith(MongodArguments.defaults() + .withAuth(withAuth))) + .build() + .start(Version.Main.V4_4, listener); + } + + private static ServerAddress getServerAddress( + final TransitionWalker.ReachedState running + ) { + final de.flapdoodle.embed.mongo.commands.ServerAddress address = running.current().getServerAddress(); + return new ServerAddress(address.getHost(), address.getPort()); + } + +} diff --git a/src/test/resources/de/flapdoodle/embed/mongo/doc/Howto.md b/src/test/resources/de/flapdoodle/embed/mongo/doc/Howto.md index 879a4e52..38167392 100644 --- a/src/test/resources/de/flapdoodle/embed/mongo/doc/Howto.md +++ b/src/test/resources/de/flapdoodle/embed/mongo/doc/Howto.md @@ -58,7 +58,10 @@ ${testCustomOutputToConsolePrefix} ... ${testCustomOutputToFile} ... +``` +```java +${testCustomOutputToFile.FileStreamProcessor} ``` #### ... to null device @@ -117,6 +120,16 @@ ${testMongosAndMongod} ```java ${importJsonIntoMongoDB} ``` + +### User/Roles setup + +```java +${setupUserAndRoles} +``` + +```java +${setupUserAndRoles.EnableAuthentication} +``` ----