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