You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by ka...@apache.org on 2023/03/12 11:50:49 UTC
[james-project] 01/07: JAMES-3881 Set a JMX password
This is an automated email from the ASF dual-hosted git repository.
kao pushed a commit to branch 3.7.x
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit e3afb128cfb5ff2bc22ad3a45d22cffa47cf2848
Author: Tung Van TRAN <vt...@linagora.com>
AuthorDate: Mon Feb 13 07:36:58 2023 +0700
JAMES-3881 Set a JMX password
(cherry picked from commit 57874c0a41fbdbf9e96740583cf2337766a5c816)
---
.../cli/JmxSecurityServerIntegrationTest.java | 115 +++++++++++++++++++++
.../main/java/org/apache/james/cli/ServerCmd.java | 39 ++++++-
.../apache/james/cli/probe/impl/JmxConnection.java | 39 ++++++-
.../java/org/apache/james/cli/ServerCmdTest.java | 59 +++++++++++
.../docs/modules/ROOT/pages/configure/jmx.adoc | 34 ++++++
server/apps/memory-app/pom.xml | 1 +
.../org/apache/james/TemporaryJamesServer.java | 106 +++++++++++++++++++
.../org/apache/james/modules/server/JMXServer.java | 71 ++++++++++++-
.../james/modules/server/JmxConfiguration.java | 5 +
9 files changed, 462 insertions(+), 7 deletions(-)
diff --git a/server/apps/cli-integration-tests/src/test/java/org/apache/james/cli/JmxSecurityServerIntegrationTest.java b/server/apps/cli-integration-tests/src/test/java/org/apache/james/cli/JmxSecurityServerIntegrationTest.java
new file mode 100644
index 0000000000..089016684e
--- /dev/null
+++ b/server/apps/cli-integration-tests/src/test/java/org/apache/james/cli/JmxSecurityServerIntegrationTest.java
@@ -0,0 +1,115 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you 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 org.apache.james.cli;
+
+import static org.apache.james.MemoryJamesServerMain.IN_MEMORY_SERVER_AGGREGATE_MODULE;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.james.GuiceJamesServer;
+import org.apache.james.TemporaryJamesServer;
+import org.apache.james.cli.util.OutputCapture;
+import org.apache.james.data.UsersRepositoryModuleChooser;
+import org.apache.james.mailbox.store.search.ListeningMessageSearchIndex;
+import org.apache.james.modules.data.MemoryUsersRepositoryModule;
+import org.apache.james.modules.server.JMXServerModule;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import com.google.common.collect.ImmutableList;
+
+class JmxSecurityServerIntegrationTest {
+
+ private static final List<String> BASE_CONFIGURATION_FILE_NAMES = ImmutableList.of("dnsservice.xml",
+ "dnsservice.xml",
+ "imapserver.xml",
+ "jwt_publickey",
+ "lmtpserver.xml",
+ "mailetcontainer.xml",
+ "mailrepositorystore.xml",
+ "managesieveserver.xml",
+ "pop3server.xml",
+ "smtpserver.xml");
+
+ private GuiceJamesServer jamesServer;
+
+ @BeforeEach
+ void beforeEach(@TempDir Path workingPath) throws Exception {
+ TemporaryJamesServer temporaryJamesServer = new TemporaryJamesServer(workingPath.toFile(), BASE_CONFIGURATION_FILE_NAMES);
+ writeFile(workingPath + "/conf/jmx.properties", "jmx.address=127.0.0.1\n" +
+ "jmx.port=9999\n");
+ writeFile(workingPath + "/conf/jmxremote.password", "james-admin pass1\n");
+ writeFile(workingPath + "/conf/jmxremote.access", "james-admin readwrite\n");
+
+ jamesServer = temporaryJamesServer.getJamesServer()
+ .combineWith(IN_MEMORY_SERVER_AGGREGATE_MODULE)
+ .combineWith(new UsersRepositoryModuleChooser(new MemoryUsersRepositoryModule())
+ .chooseModules(UsersRepositoryModuleChooser.Implementation.DEFAULT))
+ .overrideWith(new JMXServerModule(),
+ binder -> binder.bind(ListeningMessageSearchIndex.class).toInstance(mock(ListeningMessageSearchIndex.class)));
+ jamesServer.start();
+
+ }
+
+ @AfterEach
+ void afterEach() {
+ if (jamesServer != null && jamesServer.isStarted()) {
+ jamesServer.stop();
+ }
+ }
+
+ @Test
+ void jamesCliShouldFailWhenNotGiveAuthCredential() throws Exception {
+ OutputCapture outputCapture = new OutputCapture();
+
+ assertThatThrownBy(() -> ServerCmd.executeAndOutputToStream(new String[]{"-h", "127.0.0.1", "-p", "9999", "listdomains"}, outputCapture.getPrintStream()))
+ .isInstanceOf(SecurityException.class)
+ .hasMessageContaining("Authentication failed! Credentials required");
+ }
+
+ @Test
+ void jamesCliShouldWorkWhenGiveAuthCredential() throws Exception {
+ OutputCapture outputCapture = new OutputCapture();
+ ServerCmd.executeAndOutputToStream(new String[]{"-h", "127.0.0.1", "-p", "9999", "-username", "james-admin", "-password", "pass1",
+ "listdomains"}, outputCapture.getPrintStream());
+
+ assertThat(outputCapture.getContent()).contains("localhost");
+ }
+
+ private void writeFile(String fileNamePath, String data) {
+ File passwordFile = new File(fileNamePath);
+ try (OutputStream outputStream = new FileOutputStream(passwordFile)) {
+ IOUtils.write(data, outputStream, StandardCharsets.UTF_8);
+ } catch (IOException ignored) {
+ }
+ }
+}
diff --git a/server/apps/cli/src/main/java/org/apache/james/cli/ServerCmd.java b/server/apps/cli/src/main/java/org/apache/james/cli/ServerCmd.java
index 29b9ff42f3..77b3aa1496 100644
--- a/server/apps/cli/src/main/java/org/apache/james/cli/ServerCmd.java
+++ b/server/apps/cli/src/main/java/org/apache/james/cli/ServerCmd.java
@@ -18,13 +18,16 @@
****************************************************************/
package org.apache.james.cli;
+import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
+import java.util.StringTokenizer;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@@ -34,6 +37,7 @@ import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
+import org.apache.commons.io.FileUtils;
import org.apache.james.cli.exceptions.InvalidArgumentNumberException;
import org.apache.james.cli.exceptions.JamesCliException;
import org.apache.james.cli.exceptions.MissingCommandException;
@@ -72,6 +76,10 @@ public class ServerCmd {
public static final String PORT_OPT_LONG = "port";
public static final String PORT_OPT_SHORT = "p";
+ public static final String JMX_USERNAME_OPT = "username";
+ public static final String JMX_PASSWORD_OPT = "password";
+ public static final String JMX_PASSWORD_FILE_PATH_DEFAULT = System.getProperty("user.home") + "/conf/jmxremote.password";
+
private static final String DEFAULT_HOST = "127.0.0.1";
private static final int DEFAULT_PORT = 9999;
private static final Logger LOG = LoggerFactory.getLogger(ServerCmd.class);
@@ -79,7 +87,9 @@ public class ServerCmd {
private static Options createOptions() {
return new Options()
.addOption(HOST_OPT_SHORT, HOST_OPT_LONG, true, "node hostname or ip address")
- .addOption(PORT_OPT_SHORT, PORT_OPT_LONG, true, "remote jmx agent port number");
+ .addOption(PORT_OPT_SHORT, PORT_OPT_LONG, true, "remote jmx agent port number")
+ .addOption(JMX_USERNAME_OPT, JMX_USERNAME_OPT, true, "remote jmx username")
+ .addOption(JMX_PASSWORD_OPT, JMX_PASSWORD_OPT, true, "remote jmx password");
}
/**
@@ -111,7 +121,8 @@ public class ServerCmd {
public static void executeAndOutputToStream(String[] args, PrintStream printStream) throws Exception {
Stopwatch stopWatch = Stopwatch.createStarted();
CommandLine cmd = parseCommandLine(args);
- JmxConnection jmxConnection = new JmxConnection(getHost(cmd), getPort(cmd));
+ JmxConnection jmxConnection = new JmxConnection(getHost(cmd), getPort(cmd), getAuthCredential(cmd, JMX_PASSWORD_FILE_PATH_DEFAULT));
+
CmdType cmdType = new ServerCmd(
new JmxDataProbe().connect(jmxConnection),
new JmxMailboxProbe().connect(jmxConnection),
@@ -155,6 +166,30 @@ public class ServerCmd {
return host;
}
+ @VisibleForTesting
+ static Optional<JmxConnection.AuthCredential> getAuthCredential(CommandLine cmd, String jmxPasswordFilePath) {
+ return getAuthCredentialFromCommandLine(cmd)
+ .or(() -> getAuthCredentialFromJmxPasswordFile(jmxPasswordFilePath));
+ }
+
+ static Optional<JmxConnection.AuthCredential> getAuthCredentialFromCommandLine(CommandLine cmd) {
+ String username = cmd.getOptionValue(JMX_USERNAME_OPT);
+ String password = cmd.getOptionValue(JMX_PASSWORD_OPT);
+ if (Strings.isNullOrEmpty(username) || Strings.isNullOrEmpty(password)) {
+ return Optional.empty();
+ }
+ return Optional.of(new JmxConnection.AuthCredential(username, password));
+ }
+
+ static Optional<JmxConnection.AuthCredential> getAuthCredentialFromJmxPasswordFile(String jmxPasswordFilePath) {
+ try {
+ StringTokenizer stringTokenizer = new StringTokenizer(FileUtils.readLines(new File(jmxPasswordFilePath), StandardCharsets.US_ASCII).get(0), " ");
+ return Optional.of(new JmxConnection.AuthCredential(stringTokenizer.nextToken(), stringTokenizer.nextToken()));
+ } catch (Exception e) {
+ return Optional.empty();
+ }
+ }
+
@VisibleForTesting
static int getPort(CommandLine cmd) throws ParseException {
String portNum = cmd.getOptionValue(PORT_OPT_LONG);
diff --git a/server/apps/cli/src/main/java/org/apache/james/cli/probe/impl/JmxConnection.java b/server/apps/cli/src/main/java/org/apache/james/cli/probe/impl/JmxConnection.java
index ed416f39f5..a6762b1054 100644
--- a/server/apps/cli/src/main/java/org/apache/james/cli/probe/impl/JmxConnection.java
+++ b/server/apps/cli/src/main/java/org/apache/james/cli/probe/impl/JmxConnection.java
@@ -20,6 +20,9 @@ package org.apache.james.cli.probe.impl;
import java.io.Closeable;
import java.io.IOException;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
import javax.management.MBeanServerConnection;
import javax.management.MBeanServerInvocationHandler;
@@ -29,20 +32,50 @@ import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
+import com.google.common.collect.ImmutableMap;
+
public class JmxConnection implements Closeable {
+ public static class AuthCredential {
+ String username;
+ String password;
+
+ public AuthCredential(String username, String password) {
+ this.username = username;
+ this.password = password;
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o instanceof AuthCredential) {
+ AuthCredential that = (AuthCredential) o;
+ return Objects.equals(this.username, that.username)
+ && Objects.equals(this.password, that.password);
+ }
+ return false;
+ }
+
+ @Override
+ public final int hashCode() {
+ return Objects.hash(username, password);
+ }
+ }
+
private static final String fmtUrl = "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi";
private static final int defaultPort = 9999;
public static JmxConnection defaultJmxConnection(String host) throws IOException {
- return new JmxConnection(host, defaultPort);
+ return new JmxConnection(host, defaultPort, Optional.empty());
}
private final JMXConnector jmxConnector;
- public JmxConnection(String host, int port) throws IOException {
+ public JmxConnection(String host, int port, Optional<AuthCredential> authCredential) throws IOException {
JMXServiceURL jmxUrl = new JMXServiceURL(String.format(fmtUrl, host, port));
- jmxConnector = JMXConnectorFactory.connect(jmxUrl, null);
+ Map<String, ?> env = authCredential
+ .map(credential -> ImmutableMap.of("jmx.remote.credentials", new String[]{credential.username, credential.password}))
+ .orElse(ImmutableMap.of());
+ jmxConnector = JMXConnectorFactory.connect(jmxUrl, env);
}
@Override
diff --git a/server/apps/cli/src/test/java/org/apache/james/cli/ServerCmdTest.java b/server/apps/cli/src/test/java/org/apache/james/cli/ServerCmdTest.java
index 0b8d025dda..5f169ad3d3 100644
--- a/server/apps/cli/src/test/java/org/apache/james/cli/ServerCmdTest.java
+++ b/server/apps/cli/src/test/java/org/apache/james/cli/ServerCmdTest.java
@@ -25,14 +25,24 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.Optional;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.ParseException;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.RandomStringUtils;
import org.apache.james.cli.exceptions.InvalidArgumentNumberException;
import org.apache.james.cli.exceptions.MissingCommandException;
import org.apache.james.cli.exceptions.UnrecognizedCommandException;
+import org.apache.james.cli.probe.impl.JmxConnection;
import org.apache.james.cli.probe.impl.JmxDataProbe;
import org.apache.james.cli.probe.impl.JmxMailboxProbe;
import org.apache.james.cli.probe.impl.JmxQuotaProbe;
@@ -48,6 +58,7 @@ import org.apache.james.mailbox.model.SerializableQuotaLimitValue;
import org.apache.james.rrt.lib.MappingsImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
import com.google.common.collect.ImmutableList;
@@ -1200,4 +1211,52 @@ class ServerCmdTest {
.isInstanceOf(IllegalArgumentException.class);
}
+ @Test
+ void getAuthCredentialShouldReturnEmptyWhenNotGiven(@TempDir Path tempDir) throws Exception {
+ String[] arguments = {"-h", "127.0.0.1", "-p", "99999", "command", "arg1", "arg2", "arg3"};
+ CommandLine commandLine = ServerCmd.parseCommandLine(arguments);
+
+ assertThat(ServerCmd.getAuthCredential(commandLine, tempDir.toString()))
+ .isEmpty();
+ }
+
+ @Test
+ void getAuthCredentialShouldReturnValueWhenGivenViaCommandLine(@TempDir Path tempDir) throws Exception {
+ String[] arguments = {"-h", "127.0.0.1", "-p", "99999", "-username", "james-admin", "-password", "123456", "command", "arg1", "arg2", "arg3"};
+ CommandLine commandLine = ServerCmd.parseCommandLine(arguments);
+
+ assertThat(ServerCmd.getAuthCredential(commandLine, tempDir.toString()))
+ .isEqualTo(Optional.of(new JmxConnection.AuthCredential("james-admin", "123456")));
+ }
+
+ @Test
+ void getAuthCredentialShouldReturnValueWhenGivenViaJmxPasswordFile(@TempDir Path tempDir) throws Exception {
+ String[] arguments = {"-h", "127.0.0.1", "-p", "99999", "command", "arg1", "arg2", "arg3"};
+ CommandLine commandLine = ServerCmd.parseCommandLine(arguments);
+
+ File passwordFile = new File(tempDir.toString() + "/jmxremote.password");
+ try (OutputStream outputStream = new FileOutputStream(passwordFile)) {
+ IOUtils.write("james-admin1 pass2\n", outputStream, StandardCharsets.UTF_8);
+ } catch (IOException ignored) {
+ }
+
+ assertThat(ServerCmd.getAuthCredential(commandLine, passwordFile.getPath()))
+ .isEqualTo(Optional.of(new JmxConnection.AuthCredential("james-admin1", "pass2")));
+ }
+
+ @Test
+ void getAuthCredentialShouldPreferCommandlineValue(@TempDir Path tempDir) throws Exception {
+ String[] arguments = {"-h", "127.0.0.1", "-p", "99999", "-username", "james-admin", "-password", "123456", "command", "arg1", "arg2", "arg3"};
+ CommandLine commandLine = ServerCmd.parseCommandLine(arguments);
+
+ File passwordFile = new File(tempDir.toString() + "/jmxremote.password");
+ try (OutputStream outputStream = new FileOutputStream(passwordFile)) {
+ IOUtils.write("james-admin1 pass2\n", outputStream, StandardCharsets.UTF_8);
+ } catch (IOException ignored) {
+ }
+
+ assertThat(ServerCmd.getAuthCredential(commandLine, tempDir.toString()))
+ .isEqualTo(Optional.of(new JmxConnection.AuthCredential("james-admin", "123456")));
+ }
+
}
diff --git a/server/apps/distributed-app/docs/modules/ROOT/pages/configure/jmx.adoc b/server/apps/distributed-app/docs/modules/ROOT/pages/configure/jmx.adoc
index 695128588b..996b8a6ebd 100644
--- a/server/apps/distributed-app/docs/modules/ROOT/pages/configure/jmx.adoc
+++ b/server/apps/distributed-app/docs/modules/ROOT/pages/configure/jmx.adoc
@@ -22,3 +22,37 @@ in GIT to get some examples and hints.
To access from a remote location, it has been reported that `-Dcom.sun.management.jmxremote.ssl=false` is needed as
a JVM argument.
+
+
+== JMX Security
+
+In order to set up JMX authentication, we need to put `jmxremote.password` and `jmxremote.access` file
+to `/conf` directory.
+
+- `jmxremote.password`: define the username and password, that will be used by the client (here is james-cli)
+
+File's content example:
+```
+james-admin pass1
+```
+
+- `jmxremote.access`: define the pair of username and access permission
+
+File's content example:
+```
+james-admin readWrite
+```
+
+When James runs with option `-Djames.jmx.credential.generation=true`, James will automatically generate `jmxremote.password` if the file does not exist.
+Then the default username is `james-admin` and a random password.
+
+=== James-cli
+
+When the JMX server starts with authentication configuration, it will require the client need provide username/password for bypass.
+To do that, we need set arguments `-username` and `-password` for the command request.
+
+Command example:
+```
+james-cli -h 127.0.0.1 -p 9999 -username james-admin -password pass1 listdomains
+```
+
diff --git a/server/apps/memory-app/pom.xml b/server/apps/memory-app/pom.xml
index 1e1729e469..1736218be8 100644
--- a/server/apps/memory-app/pom.xml
+++ b/server/apps/memory-app/pom.xml
@@ -297,6 +297,7 @@
<jvmFlag>-Dworking.directory=/root/</jvmFlag>
<!-- Prevents Logjam (CVE-2015-4000) -->
<jvmFlag>-Djdk.tls.ephemeralDHKeySize=2048</jvmFlag>
+ <jvmFlag>-Djames.jmx.credential.generation=true</jvmFlag>
</jvmFlags>
<creationTime>USE_CURRENT_TIMESTAMP</creationTime>
<volumes>
diff --git a/server/container/guice/common/src/main/java/org/apache/james/TemporaryJamesServer.java b/server/container/guice/common/src/main/java/org/apache/james/TemporaryJamesServer.java
new file mode 100644
index 0000000000..231d3586c2
--- /dev/null
+++ b/server/container/guice/common/src/main/java/org/apache/james/TemporaryJamesServer.java
@@ -0,0 +1,106 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you 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 org.apache.james;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.james.server.core.configuration.Configuration;
+
+import com.google.common.collect.ImmutableList;
+
+public class TemporaryJamesServer {
+ private static final List<String> CONFIGURATION_FILE_NAMES = ImmutableList.of(
+ "dnsservice.xml",
+ "domainlist.xml",
+ "imapserver.xml",
+ "keystore",
+ "listeners.xml",
+ "lmtpserver.xml",
+ "mailetcontainer.xml",
+ "mailrepositorystore.xml",
+ "managesieveserver.xml",
+ "pop3server.xml",
+ "smtpserver.xml",
+ "usersrepository.xml");
+
+ private final Configuration configuration;
+ private final File configurationFolder;
+ private final List<String> configurationFileNames;
+ private GuiceJamesServer jamesServer;
+
+ public TemporaryJamesServer(File workingDir) {
+ this(workingDir, CONFIGURATION_FILE_NAMES);
+ }
+
+ public TemporaryJamesServer(File workingDir, List<String> configurationFileNames) {
+ this.configurationFileNames = configurationFileNames;
+ Configuration configuration = Configuration.builder().workingDirectory(workingDir).build();
+ configurationFolder = workingDir.toPath().resolve("conf").toFile();
+ if (!configurationFolder.exists()) {
+ configurationFolder.mkdir();
+ }
+ copyResources(Paths.get(configurationFolder.getAbsolutePath()));
+ this.configuration = configuration;
+ }
+
+ public GuiceJamesServer getJamesServer() {
+ if (jamesServer == null) {
+ jamesServer = GuiceJamesServer.forConfiguration(configuration);
+ }
+ return jamesServer;
+ }
+
+ public void appendConfigurationFile(String configurationData, String configurationFileName) throws IOException {
+ try (OutputStream outputStream = new FileOutputStream(Paths.get(configurationFolder.getAbsolutePath(), configurationFileName).toFile())) {
+ IOUtils.write(configurationData, outputStream, StandardCharsets.UTF_8);
+ }
+ }
+
+ private void copyResources(Path resourcesFolder) {
+ configurationFileNames
+ .forEach(resourceName -> copyResource(resourcesFolder, resourceName));
+ }
+
+ public static void copyResource(Path resourcesFolder, String resourceName) {
+ var resolvedResource = resourcesFolder.resolve(resourceName);
+ try (OutputStream outputStream = new FileOutputStream(resolvedResource.toFile())) {
+ URL resource = ClassLoader.getSystemClassLoader().getResource(resourceName);
+ if (resource != null) {
+ try (InputStream stream = resource.openStream()) {
+ stream.transferTo(outputStream);
+ }
+ } else {
+ throw new RuntimeException("Failed to load configuration resource " + resourceName);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/server/container/guice/jmx/src/main/java/org/apache/james/modules/server/JMXServer.java b/server/container/guice/jmx/src/main/java/org/apache/james/modules/server/JMXServer.java
index e1ffb3ca2f..badd87f9b3 100644
--- a/server/container/guice/jmx/src/main/java/org/apache/james/modules/server/JMXServer.java
+++ b/server/container/guice/jmx/src/main/java/org/apache/james/modules/server/JMXServer.java
@@ -19,11 +19,25 @@
package org.apache.james.modules.server;
+import static org.apache.james.modules.server.JmxConfiguration.ACCESS_FILE_NAME;
+import static org.apache.james.modules.server.JmxConfiguration.JMX_CREDENTIAL_GENERATION_ENABLE_DEFAULT_VALUE;
+import static org.apache.james.modules.server.JmxConfiguration.JMX_CREDENTIAL_GENERATION_ENABLE_PROPERTY_KEY;
+import static org.apache.james.modules.server.JmxConfiguration.PASSWORD_FILE_NAME;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
import java.lang.management.ManagementFactory;
import java.net.ServerSocket;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
import java.rmi.registry.LocateRegistry;
import java.util.HashSet;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import javax.annotation.PreDestroy;
@@ -34,7 +48,11 @@ import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.james.filesystem.api.JamesDirectoriesProvider;
import org.apache.james.lifecycle.api.Startable;
+import org.apache.james.util.FunctionalUtils;
import org.apache.james.util.RestrictingRMISocketFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -48,13 +66,17 @@ public class JMXServer implements Startable {
private final JmxConfiguration jmxConfiguration;
private final Set<String> registeredKeys;
private final Object lock;
+ private final String jmxPasswordFilePath;
+ private final String jmxAccessFilePath;
private JMXConnectorServer jmxConnectorServer;
private boolean isStarted;
private RestrictingRMISocketFactory restrictingRMISocketFactory;
@Inject
- public JMXServer(JmxConfiguration jmxConfiguration) {
+ public JMXServer(JmxConfiguration jmxConfiguration, JamesDirectoriesProvider directoriesProvider) {
this.jmxConfiguration = jmxConfiguration;
+ this.jmxPasswordFilePath = directoriesProvider.getConfDirectory() + PASSWORD_FILE_NAME;
+ this.jmxAccessFilePath = directoriesProvider.getConfDirectory() + ACCESS_FILE_NAME;
isStarted = false;
registeredKeys = new HashSet<>();
lock = new Object();
@@ -98,8 +120,14 @@ public class JMXServer implements Startable {
+ ":" + jmxConfiguration.getHost().getPort() + "/jmxrmi";
restrictingRMISocketFactory = new RestrictingRMISocketFactory(jmxConfiguration.getHost().getHostName());
LocateRegistry.createRegistry(jmxConfiguration.getHost().getPort(), restrictingRMISocketFactory, restrictingRMISocketFactory);
+ generateJMXPasswordFileIfNeed();
+
+ Map<String, String> environment = Optional.of(existJmxPasswordFile())
+ .filter(FunctionalUtils.identityPredicate())
+ .map(hasJmxPasswordFile -> ImmutableMap.of("jmx.remote.x.password.file", jmxPasswordFilePath,
+ "jmx.remote.x.access.file", jmxAccessFilePath))
+ .orElse(ImmutableMap.of());
- Map<String, ?> environment = ImmutableMap.of();
jmxConnectorServer = JMXConnectorServerFactory.newJMXConnectorServer(new JMXServiceURL(serviceURL),
environment,
ManagementFactory.getPlatformMBeanServer());
@@ -126,4 +154,43 @@ public class JMXServer implements Startable {
}
}
+ private void generateJMXPasswordFileIfNeed() {
+ if (Boolean.parseBoolean(System.getProperty(JMX_CREDENTIAL_GENERATION_ENABLE_PROPERTY_KEY, JMX_CREDENTIAL_GENERATION_ENABLE_DEFAULT_VALUE))
+ && !existJmxPasswordFile()) {
+ generateJMXPasswordFile();
+ }
+ }
+
+ private boolean existJmxPasswordFile() {
+ return Files.exists(Path.of(jmxPasswordFilePath)) && Files.exists(Path.of(jmxAccessFilePath));
+ }
+
+ private void generateJMXPasswordFile() {
+ File passwordFile = new File(jmxPasswordFilePath);
+ if (!passwordFile.exists()) {
+ try (OutputStream outputStream = new FileOutputStream(passwordFile)) {
+ String randomPassword = RandomStringUtils.random(10, true, true);
+ IOUtils.write(JmxConfiguration.JAMES_ADMIN_USER_DEFAULT + " " + randomPassword + "\n", outputStream, StandardCharsets.UTF_8);
+ setPermissionOwnerOnly(passwordFile);
+ LOGGER.info("Generated JMX password file: " + passwordFile.getPath());
+ } catch (IOException e) {
+ throw new RuntimeException("Error when creating JMX password file: " + passwordFile.getPath(), e);
+ }
+ }
+
+ File accessFile = new File(jmxAccessFilePath);
+ if (!accessFile.exists()) {
+ try (OutputStream outputStream = new FileOutputStream(accessFile)) {
+ IOUtils.write(JmxConfiguration.JAMES_ADMIN_USER_DEFAULT + " readwrite\n", outputStream, StandardCharsets.UTF_8);
+ setPermissionOwnerOnly(accessFile);
+ LOGGER.info("Generated JMX access file: " + accessFile.getPath());
+ } catch (IOException e) {
+ throw new RuntimeException("Error when creating JMX access file: " + accessFile.getPath(), e);
+ }
+ }
+ }
+
+ private void setPermissionOwnerOnly(File file) throws IOException {
+ Files.setPosixFilePermissions(file.toPath(), Set.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE));
+ }
}
diff --git a/server/container/guice/jmx/src/main/java/org/apache/james/modules/server/JmxConfiguration.java b/server/container/guice/jmx/src/main/java/org/apache/james/modules/server/JmxConfiguration.java
index 9bbebd3a28..c833990e20 100644
--- a/server/container/guice/jmx/src/main/java/org/apache/james/modules/server/JmxConfiguration.java
+++ b/server/container/guice/jmx/src/main/java/org/apache/james/modules/server/JmxConfiguration.java
@@ -34,6 +34,11 @@ public class JmxConfiguration {
public static final String LOCALHOST = "localhost";
public static final int DEFAULT_PORT = 9999;
public static final boolean ENABLED = true;
+ public static final String JMX_CREDENTIAL_GENERATION_ENABLE_PROPERTY_KEY = "james.jmx.credential.generation";
+ public static final String JMX_CREDENTIAL_GENERATION_ENABLE_DEFAULT_VALUE = "false";
+ public static final String PASSWORD_FILE_NAME = "jmxremote.password";
+ public static final String ACCESS_FILE_NAME = "jmxremote.access";
+ public static final String JAMES_ADMIN_USER_DEFAULT = "james-admin";
public static final JmxConfiguration DEFAULT_CONFIGURATION = new JmxConfiguration(ENABLED, Optional.of(Host.from(LOCALHOST, DEFAULT_PORT)));
public static final JmxConfiguration DISABLED = new JmxConfiguration(!ENABLED, Optional.empty());
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org