This is an automated email from the ASF dual-hosted git repository. chibenwa pushed a commit to branch 3.9.x in repository https://gitbox.apache.org/repos/asf/james-project.git
commit e666c773abe5c604ff64badc1db9ff212af671a5 Author: Benoit TELLIER <[email protected]> AuthorDate: Tue Jul 28 23:11:07 2026 +0200 [ENHANCEMENT] Webadmin: default to auto-generate creds --- CHANGELOG.md | 7 ++- .../servers/pages/distributed/run/run-docker.adoc | 3 +- .../servers/pages/postgres/run/run-docker.adoc | 3 +- .../servers/partials/configure/webadmin.adoc | 20 ++++++ .../docker-configuration/webadmin.properties | 10 ++- .../sample-configuration/webadmin.properties | 13 +++- .../docker-configuration/webadmin.properties | 10 ++- .../sample-configuration/webadmin.properties | 13 +++- .../docker-configuration/webadmin.properties | 10 ++- .../sample-configuration/webadmin.properties | 13 +++- .../docker-configuration/webadmin.properties | 10 ++- .../sample-configuration/webadmin.properties | 13 +++- .../docker-configuration/webadmin.properties | 10 ++- .../sample-configuration/webadmin.properties | 13 +++- .../docker-configuration/webadmin.properties | 10 ++- .../sample-configuration/webadmin.properties | 13 +++- .../src/test/resources/webadmin.properties | 5 +- .../sample-configuration/webadmin.properties | 13 +++- .../src/test/resources/webadmin.properties | 5 +- .../james/modules/server/WebAdminServerModule.java | 34 +++++++++- .../modules/server/WebAdminServerModuleTest.java | 72 ++++++++++++++++++++++ .../src/test/resources/webadmin.properties | 5 +- .../src/test/resources/webadmin.properties | 5 +- .../src/test/resources/webadmin.properties | 5 +- .../webadmin/authentication/PasswordGenerator.java | 45 ++++++++++++++ 25 files changed, 337 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0076472780..dd3d352483 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) ## [Unreleased 3.9.x] -No changes yet. +### Security + + - Webadmin `password.generate`: generate a random password upon start up when none is configured, and log it. + **Breaking change**: this defaults to `true`, thus webadmin is no longer unauthenticated out of the box, existing + deployments included. Set `password.generate=false` in `webadmin.properties` to opt back into an unauthenticated + webadmin, or configure `password` to pin a stable secret. ## [3.9.0] - 2025-xx-xx diff --git a/docs/modules/servers/pages/distributed/run/run-docker.adoc b/docs/modules/servers/pages/distributed/run/run-docker.adoc index f05f2f71d7..66043ea2de 100644 --- a/docs/modules/servers/pages/distributed/run/run-docker.adoc +++ b/docs/modules/servers/pages/distributed/run/run-docker.adoc @@ -82,7 +82,8 @@ Where : - HOSTNAME: is the hostname you want to give to your James container. This DNS entry will be used to send mail to your James server. -Webadmin port binding is restricted to loopback as users are not authenticated by default on webadmin server. Thus you should avoid exposing it in production. +Webadmin port binding is restricted to loopback. Webadmin is protected by a password randomly generated upon +each start up and written in the logs (see `password.generate`), yet you should avoid exposing it in production. Note that the above example assumes `127.0.0.1` is your loopback interface for convenience but you should change it if this is not the case on your machine. If you want to pass additional options to the underlying java command, you can configure a _JAVA_TOOL_OPTIONS_ env variable, for example add: diff --git a/docs/modules/servers/pages/postgres/run/run-docker.adoc b/docs/modules/servers/pages/postgres/run/run-docker.adoc index f6bd9d8f1f..bd56b4f144 100644 --- a/docs/modules/servers/pages/postgres/run/run-docker.adoc +++ b/docs/modules/servers/pages/postgres/run/run-docker.adoc @@ -79,7 +79,8 @@ Where : - HOSTNAME: is the hostname you want to give to your James container. This DNS entry will be used to send mail to your James server. -Webadmin port binding is restricted to loopback as users are not authenticated by default on webadmin server. Thus you should avoid exposing it in production. +Webadmin port binding is restricted to loopback. Webadmin is protected by a password randomly generated upon +each start up and written in the logs (see `password.generate`), yet you should avoid exposing it in production. Note that the above example assumes `127.0.0.1` is your loopback interface for convenience but you should change it if this is not the case on your machine. If you want to pass additional options to the underlying java command, you can configure a _JAVA_TOOL_OPTIONS_ env variable, for example add: diff --git a/docs/modules/servers/partials/configure/webadmin.adoc b/docs/modules/servers/partials/configure/webadmin.adoc index dc429c142a..2825af1e17 100644 --- a/docs/modules/servers/partials/configure/webadmin.adoc +++ b/docs/modules/servers/partials/configure/webadmin.adoc @@ -39,6 +39,26 @@ to get some examples and hints. | cors.origin | Specify ths CORS origin (default: null) +| password.generate +| Generates a random password upon start up when no `password` is configured, allowing a secure setup without +hardcoded credentials (default: true). As such, WebAdmin is never unauthenticated unless explicitly asked for. + +.... +password.generate=false +.... + +The generated password is written in the logs upon start up: + +.... +WARN No WebAdmin password had been configured: a random one had been generated for this run. [...] +Generated WebAdmin password: 8Kj2mXqT4vZ... +.... + +Beware: the generated password changes upon each restart, and is exposed to whoever can read the logs. Configure +`password` explicitly for setups needing a stable secret, or set `password.generate=false` to opt back into an +unauthenticated WebAdmin. This option is ignored when `password` is configured, when `jwt.enabled` is true, and when +WebAdmin is disabled. + | password | Uses a configured static value for authentication. It relies on the Password header. It supports several passwords, configured as a coma separated list. diff --git a/server/apps/distributed-app/docker-configuration/webadmin.properties b/server/apps/distributed-app/docker-configuration/webadmin.properties index 5d72d99b74..42c276ec18 100644 --- a/server/apps/distributed-app/docker-configuration/webadmin.properties +++ b/server/apps/distributed-app/docker-configuration/webadmin.properties @@ -51,4 +51,12 @@ https.enabled=false # List of fully qualified class names that should be exposed over webadmin # in addition to your product default routes. Routes needs to be located # within the classpath or in the ./extensions-jars folder. -#extensions.routes= \ No newline at end of file +#extensions.routes= + +# Password authentication settings + +# Generate a random password upon start up when no password is configured below. +# The generated password is written in the logs, and changes upon each restart. +# Set it to false to run WebAdmin without any authentication. +# Defaults to true +password.generate=true diff --git a/server/apps/distributed-app/sample-configuration/webadmin.properties b/server/apps/distributed-app/sample-configuration/webadmin.properties index b97c71829c..0727d221be 100644 --- a/server/apps/distributed-app/sample-configuration/webadmin.properties +++ b/server/apps/distributed-app/sample-configuration/webadmin.properties @@ -52,4 +52,15 @@ https.enabled=false # List of fully qualified class names that should be exposed over webadmin # in addition to your product default routes. Routes needs to be located # within the classpath or in the ./extensions-jars folder. -#extensions.routes= \ No newline at end of file +#extensions.routes= + +# Password authentication settings + +# Generate a random password upon start up when no password is configured below. +# The generated password is written in the logs, and changes upon each restart. +# Set it to false to run WebAdmin without any authentication. +# Defaults to true +password.generate=true + +# Configure one or more passwords (comma separated) for WebAdmin authentication +#password=secret1,secret2 diff --git a/server/apps/distributed-pop3-app/docker-configuration/webadmin.properties b/server/apps/distributed-pop3-app/docker-configuration/webadmin.properties index 5d72d99b74..42c276ec18 100644 --- a/server/apps/distributed-pop3-app/docker-configuration/webadmin.properties +++ b/server/apps/distributed-pop3-app/docker-configuration/webadmin.properties @@ -51,4 +51,12 @@ https.enabled=false # List of fully qualified class names that should be exposed over webadmin # in addition to your product default routes. Routes needs to be located # within the classpath or in the ./extensions-jars folder. -#extensions.routes= \ No newline at end of file +#extensions.routes= + +# Password authentication settings + +# Generate a random password upon start up when no password is configured below. +# The generated password is written in the logs, and changes upon each restart. +# Set it to false to run WebAdmin without any authentication. +# Defaults to true +password.generate=true diff --git a/server/apps/distributed-pop3-app/sample-configuration/webadmin.properties b/server/apps/distributed-pop3-app/sample-configuration/webadmin.properties index b97c71829c..0727d221be 100644 --- a/server/apps/distributed-pop3-app/sample-configuration/webadmin.properties +++ b/server/apps/distributed-pop3-app/sample-configuration/webadmin.properties @@ -52,4 +52,15 @@ https.enabled=false # List of fully qualified class names that should be exposed over webadmin # in addition to your product default routes. Routes needs to be located # within the classpath or in the ./extensions-jars folder. -#extensions.routes= \ No newline at end of file +#extensions.routes= + +# Password authentication settings + +# Generate a random password upon start up when no password is configured below. +# The generated password is written in the logs, and changes upon each restart. +# Set it to false to run WebAdmin without any authentication. +# Defaults to true +password.generate=true + +# Configure one or more passwords (comma separated) for WebAdmin authentication +#password=secret1,secret2 diff --git a/server/apps/jpa-app/docker-configuration/webadmin.properties b/server/apps/jpa-app/docker-configuration/webadmin.properties index 5d72d99b74..42c276ec18 100644 --- a/server/apps/jpa-app/docker-configuration/webadmin.properties +++ b/server/apps/jpa-app/docker-configuration/webadmin.properties @@ -51,4 +51,12 @@ https.enabled=false # List of fully qualified class names that should be exposed over webadmin # in addition to your product default routes. Routes needs to be located # within the classpath or in the ./extensions-jars folder. -#extensions.routes= \ No newline at end of file +#extensions.routes= + +# Password authentication settings + +# Generate a random password upon start up when no password is configured below. +# The generated password is written in the logs, and changes upon each restart. +# Set it to false to run WebAdmin without any authentication. +# Defaults to true +password.generate=true diff --git a/server/apps/jpa-app/sample-configuration/webadmin.properties b/server/apps/jpa-app/sample-configuration/webadmin.properties index 5dc74740c5..c8e71f6842 100644 --- a/server/apps/jpa-app/sample-configuration/webadmin.properties +++ b/server/apps/jpa-app/sample-configuration/webadmin.properties @@ -46,4 +46,15 @@ https.enabled=false # List of fully qualified class names that should be exposed over webadmin # in addition to your product default routes. Routes needs to be located # within the classpath or in the ./extensions-jars folder. -#extensions.routes= \ No newline at end of file +#extensions.routes= + +# Password authentication settings + +# Generate a random password upon start up when no password is configured below. +# The generated password is written in the logs, and changes upon each restart. +# Set it to false to run WebAdmin without any authentication. +# Defaults to true +password.generate=true + +# Configure one or more passwords (comma separated) for WebAdmin authentication +#password=secret1,secret2 diff --git a/server/apps/jpa-smtp-app/docker-configuration/webadmin.properties b/server/apps/jpa-smtp-app/docker-configuration/webadmin.properties index 5d72d99b74..42c276ec18 100644 --- a/server/apps/jpa-smtp-app/docker-configuration/webadmin.properties +++ b/server/apps/jpa-smtp-app/docker-configuration/webadmin.properties @@ -51,4 +51,12 @@ https.enabled=false # List of fully qualified class names that should be exposed over webadmin # in addition to your product default routes. Routes needs to be located # within the classpath or in the ./extensions-jars folder. -#extensions.routes= \ No newline at end of file +#extensions.routes= + +# Password authentication settings + +# Generate a random password upon start up when no password is configured below. +# The generated password is written in the logs, and changes upon each restart. +# Set it to false to run WebAdmin without any authentication. +# Defaults to true +password.generate=true diff --git a/server/apps/jpa-smtp-app/sample-configuration/webadmin.properties b/server/apps/jpa-smtp-app/sample-configuration/webadmin.properties index 5dc74740c5..c8e71f6842 100644 --- a/server/apps/jpa-smtp-app/sample-configuration/webadmin.properties +++ b/server/apps/jpa-smtp-app/sample-configuration/webadmin.properties @@ -46,4 +46,15 @@ https.enabled=false # List of fully qualified class names that should be exposed over webadmin # in addition to your product default routes. Routes needs to be located # within the classpath or in the ./extensions-jars folder. -#extensions.routes= \ No newline at end of file +#extensions.routes= + +# Password authentication settings + +# Generate a random password upon start up when no password is configured below. +# The generated password is written in the logs, and changes upon each restart. +# Set it to false to run WebAdmin without any authentication. +# Defaults to true +password.generate=true + +# Configure one or more passwords (comma separated) for WebAdmin authentication +#password=secret1,secret2 diff --git a/server/apps/memory-app/docker-configuration/webadmin.properties b/server/apps/memory-app/docker-configuration/webadmin.properties index 5d72d99b74..42c276ec18 100644 --- a/server/apps/memory-app/docker-configuration/webadmin.properties +++ b/server/apps/memory-app/docker-configuration/webadmin.properties @@ -51,4 +51,12 @@ https.enabled=false # List of fully qualified class names that should be exposed over webadmin # in addition to your product default routes. Routes needs to be located # within the classpath or in the ./extensions-jars folder. -#extensions.routes= \ No newline at end of file +#extensions.routes= + +# Password authentication settings + +# Generate a random password upon start up when no password is configured below. +# The generated password is written in the logs, and changes upon each restart. +# Set it to false to run WebAdmin without any authentication. +# Defaults to true +password.generate=true diff --git a/server/apps/memory-app/sample-configuration/webadmin.properties b/server/apps/memory-app/sample-configuration/webadmin.properties index 3449eedc92..8227ddd772 100644 --- a/server/apps/memory-app/sample-configuration/webadmin.properties +++ b/server/apps/memory-app/sample-configuration/webadmin.properties @@ -53,4 +53,15 @@ https.enabled=false # List of fully qualified class names that should be exposed over webadmin # in addition to your product default routes. Routes needs to be located # within the classpath or in the ./extensions-jars folder. -#extensions.routes= \ No newline at end of file +#extensions.routes= + +# Password authentication settings + +# Generate a random password upon start up when no password is configured below. +# The generated password is written in the logs, and changes upon each restart. +# Set it to false to run WebAdmin without any authentication. +# Defaults to true +password.generate=true + +# Configure one or more passwords (comma separated) for WebAdmin authentication +#password=secret1,secret2 diff --git a/server/apps/postgres-app/docker-configuration/webadmin.properties b/server/apps/postgres-app/docker-configuration/webadmin.properties index 5d72d99b74..42c276ec18 100644 --- a/server/apps/postgres-app/docker-configuration/webadmin.properties +++ b/server/apps/postgres-app/docker-configuration/webadmin.properties @@ -51,4 +51,12 @@ https.enabled=false # List of fully qualified class names that should be exposed over webadmin # in addition to your product default routes. Routes needs to be located # within the classpath or in the ./extensions-jars folder. -#extensions.routes= \ No newline at end of file +#extensions.routes= + +# Password authentication settings + +# Generate a random password upon start up when no password is configured below. +# The generated password is written in the logs, and changes upon each restart. +# Set it to false to run WebAdmin without any authentication. +# Defaults to true +password.generate=true diff --git a/server/apps/postgres-app/sample-configuration/webadmin.properties b/server/apps/postgres-app/sample-configuration/webadmin.properties index 5dc74740c5..c8e71f6842 100644 --- a/server/apps/postgres-app/sample-configuration/webadmin.properties +++ b/server/apps/postgres-app/sample-configuration/webadmin.properties @@ -46,4 +46,15 @@ https.enabled=false # List of fully qualified class names that should be exposed over webadmin # in addition to your product default routes. Routes needs to be located # within the classpath or in the ./extensions-jars folder. -#extensions.routes= \ No newline at end of file +#extensions.routes= + +# Password authentication settings + +# Generate a random password upon start up when no password is configured below. +# The generated password is written in the logs, and changes upon each restart. +# Set it to false to run WebAdmin without any authentication. +# Defaults to true +password.generate=true + +# Configure one or more passwords (comma separated) for WebAdmin authentication +#password=secret1,secret2 diff --git a/server/apps/postgres-app/src/test/resources/webadmin.properties b/server/apps/postgres-app/src/test/resources/webadmin.properties index 3386a14238..435136da30 100644 --- a/server/apps/postgres-app/src/test/resources/webadmin.properties +++ b/server/apps/postgres-app/src/test/resources/webadmin.properties @@ -22,4 +22,7 @@ enabled=true port=0 -host=127.0.0.1 \ No newline at end of file +host=127.0.0.1 + +# These tests exercise WebAdmin routes without authentication +password.generate=false diff --git a/server/apps/scaling-pulsar-smtp/sample-configuration/webadmin.properties b/server/apps/scaling-pulsar-smtp/sample-configuration/webadmin.properties index d042dd1525..2f3d76dfd3 100644 --- a/server/apps/scaling-pulsar-smtp/sample-configuration/webadmin.properties +++ b/server/apps/scaling-pulsar-smtp/sample-configuration/webadmin.properties @@ -46,4 +46,15 @@ https.enabled=false # List of fully qualified class names that should be exposed over webadmin # in addition to your product default routes. Routes needs to be located # within the classpath or in the ./extensions-jars folder. -#extensions.routes= \ No newline at end of file +#extensions.routes= + +# Password authentication settings + +# Generate a random password upon start up when no password is configured below. +# The generated password is written in the logs, and changes upon each restart. +# Set it to false to run WebAdmin without any authentication. +# Defaults to true +password.generate=true + +# Configure one or more passwords (comma separated) for WebAdmin authentication +#password=secret1,secret2 diff --git a/server/apps/webadmin-cli/src/test/resources/webadmin.properties b/server/apps/webadmin-cli/src/test/resources/webadmin.properties index 3386a14238..435136da30 100644 --- a/server/apps/webadmin-cli/src/test/resources/webadmin.properties +++ b/server/apps/webadmin-cli/src/test/resources/webadmin.properties @@ -22,4 +22,7 @@ enabled=true port=0 -host=127.0.0.1 \ No newline at end of file +host=127.0.0.1 + +# These tests exercise WebAdmin routes without authentication +password.generate=false diff --git a/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/WebAdminServerModule.java b/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/WebAdminServerModule.java index 9b9dcca561..02f2d821a4 100644 --- a/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/WebAdminServerModule.java +++ b/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/WebAdminServerModule.java @@ -60,6 +60,7 @@ import org.apache.james.webadmin.authentication.AuthenticationFilter; import org.apache.james.webadmin.authentication.JwtFilter; import org.apache.james.webadmin.authentication.NoAuthenticationFilter; import org.apache.james.webadmin.authentication.PasswordFilter; +import org.apache.james.webadmin.authentication.PasswordGenerator; import org.apache.james.webadmin.dto.DTOModuleInjections; import org.apache.james.webadmin.mdc.RequestLogger; import org.apache.james.webadmin.utils.JsonTransformer; @@ -83,6 +84,7 @@ public class WebAdminServerModule extends AbstractModule { private static final boolean DEFAULT_JWT_DISABLED = false; private static final boolean DEFAULT_DISABLED = false; + private static final boolean DEFAULT_PASSWORD_GENERATION_ENABLED = true; private static final String DEFAULT_NO_CORS_ORIGIN = null; private static final boolean DEFAULT_CORS_DISABLED = false; private static final String DEFAULT_NO_KEYSTORE = null; @@ -142,9 +144,10 @@ public class WebAdminServerModule extends AbstractModule { Configuration configurationFile = propertiesProvider.getConfiguration("webadmin"); List<String> additionalRoutes = additionalRoutes(configurationFile); + boolean webAdminEnabled = configurationFile.getBoolean("enabled", DEFAULT_DISABLED); return WebAdminConfiguration.builder() - .enable(configurationFile.getBoolean("enabled", DEFAULT_DISABLED)) + .enable(webAdminEnabled) .port(port(configurationFile)) .tls(readHttpsConfiguration(configurationFile)) .enableCORS(configurationFile.getBoolean("cors.enable", DEFAULT_CORS_DISABLED)) @@ -155,7 +158,7 @@ public class WebAdminServerModule extends AbstractModule { Optional.ofNullable(configurationFile.getString("jwt.publickeypem.url", null)))) .maxThreadCount(Optional.ofNullable(configurationFile.getInteger("maxThreadCount", null))) .minThreadCount(Optional.ofNullable(configurationFile.getInteger("minThreadCount", null))) - .password(Optional.ofNullable(configurationFile.getString("password", null))) + .password(password(configurationFile, webAdminEnabled)) .build(); } catch (FileNotFoundException e) { LOGGER.info("No webadmin.properties file. Disabling WebAdmin interface."); @@ -172,6 +175,33 @@ public class WebAdminServerModule extends AbstractModule { return new FixedPortSupplier(portNumber); } + @VisibleForTesting + Optional<String> password(Configuration configurationFile, boolean webAdminEnabled) { + Optional<String> configuredPassword = Optional.ofNullable(configurationFile.getString("password", null)); + + if (configuredPassword.isPresent()) { + return configuredPassword; + } + if (shouldGeneratePassword(configurationFile, webAdminEnabled)) { + return Optional.of(generateAndLogPassword()); + } + return Optional.empty(); + } + + private boolean shouldGeneratePassword(Configuration configurationFile, boolean webAdminEnabled) { + return configurationFile.getBoolean("password.generate", DEFAULT_PASSWORD_GENERATION_ENABLED) + && webAdminEnabled + && !configurationFile.getBoolean("jwt.enabled", DEFAULT_JWT_DISABLED); + } + + private String generateAndLogPassword() { + String password = PasswordGenerator.generate(); + LOGGER.warn("No WebAdmin password had been configured: a random one had been generated for this run. " + + "Supply it within the `Password` header of your WebAdmin requests, or pin it with the `password` entry " + + "of webadmin.properties. Generated WebAdmin password: {}", password); + return password; + } + @VisibleForTesting ImmutableList<String> additionalRoutes(Configuration configurationFile) { return ImmutableList.copyOf(configurationFile.getStringArray("extensions.routes")); diff --git a/server/container/guice/protocols/webadmin/src/test/java/org/apache/james/modules/server/WebAdminServerModuleTest.java b/server/container/guice/protocols/webadmin/src/test/java/org/apache/james/modules/server/WebAdminServerModuleTest.java index 7d81f0b423..bbe213d2ed 100644 --- a/server/container/guice/protocols/webadmin/src/test/java/org/apache/james/modules/server/WebAdminServerModuleTest.java +++ b/server/container/guice/protocols/webadmin/src/test/java/org/apache/james/modules/server/WebAdminServerModuleTest.java @@ -21,11 +21,18 @@ package org.apache.james.modules.server; import static org.assertj.core.api.Assertions.assertThat; +import java.util.Optional; + import org.apache.commons.configuration2.Configuration; +import org.apache.commons.configuration2.PropertiesConfiguration; import org.apache.james.utils.PropertiesProvider; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; class WebAdminServerModuleTest { + private static final boolean WEBADMIN_ENABLED = true; + private static final boolean WEBADMIN_DISABLED = false; + @Test void shouldReturnEmptyWhenNoField() throws Exception { Configuration configuration = getConfiguration("webadmin-none"); @@ -61,4 +68,69 @@ class WebAdminServerModuleTest { private Configuration getConfiguration(String name) throws Exception { return PropertiesProvider.forTesting().getConfiguration(name); } + + @Nested + class PasswordGeneration { + @Test + void passwordShouldBeGeneratedByDefault() { + assertThat(new WebAdminServerModule().password(new PropertiesConfiguration(), WEBADMIN_ENABLED)) + .isNotEmpty(); + } + + @Test + void passwordShouldBeEmptyWhenGenerationIsDisabled() { + assertThat(new WebAdminServerModule().password(configuration("password.generate", false), WEBADMIN_ENABLED)) + .isEmpty(); + } + + @Test + void passwordShouldBeGeneratedWhenGenerationIsEnabled() { + assertThat(new WebAdminServerModule().password(configuration("password.generate", true), WEBADMIN_ENABLED)) + .isNotEmpty(); + } + + @Test + void generatedPasswordShouldNotContainThePasswordSeparator() { + assertThat(new WebAdminServerModule().password(configuration("password.generate", true), WEBADMIN_ENABLED)) + .hasValueSatisfying(password -> assertThat(password).doesNotContain(",")); + } + + @Test + void generatedPasswordsShouldBeRandom() { + Optional<String> firstPassword = new WebAdminServerModule().password(configuration("password.generate", true), WEBADMIN_ENABLED); + Optional<String> secondPassword = new WebAdminServerModule().password(configuration("password.generate", true), WEBADMIN_ENABLED); + + assertThat(firstPassword).isNotEqualTo(secondPassword); + } + + @Test + void configuredPasswordShouldTakePrecedenceOverGeneration() { + PropertiesConfiguration configuration = configuration("password.generate", true); + configuration.addProperty("password", "secret"); + + assertThat(new WebAdminServerModule().password(configuration, WEBADMIN_ENABLED)) + .contains("secret"); + } + + @Test + void passwordShouldNotBeGeneratedWhenWebAdminIsDisabled() { + assertThat(new WebAdminServerModule().password(configuration("password.generate", true), WEBADMIN_DISABLED)) + .isEmpty(); + } + + @Test + void passwordShouldNotBeGeneratedWhenJwtIsEnabled() { + PropertiesConfiguration configuration = configuration("password.generate", true); + configuration.addProperty("jwt.enabled", true); + + assertThat(new WebAdminServerModule().password(configuration, WEBADMIN_ENABLED)) + .isEmpty(); + } + + private PropertiesConfiguration configuration(String key, Object value) { + PropertiesConfiguration configuration = new PropertiesConfiguration(); + configuration.addProperty(key, value); + return configuration; + } + } } \ No newline at end of file diff --git a/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/resources/webadmin.properties b/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/resources/webadmin.properties index 78a176aabd..6602aa0f0a 100644 --- a/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/resources/webadmin.properties +++ b/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/resources/webadmin.properties @@ -24,4 +24,7 @@ enabled=true port=0 host=127.0.0.1 -extensions.routes=org.apache.james.webadmin.dropwizard.MetricsRoutes \ No newline at end of file +extensions.routes=org.apache.james.webadmin.dropwizard.MetricsRoutes + +# These tests exercise WebAdmin routes without authentication +password.generate=false diff --git a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/resources/webadmin.properties b/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/resources/webadmin.properties index 78a176aabd..6602aa0f0a 100644 --- a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/resources/webadmin.properties +++ b/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/resources/webadmin.properties @@ -24,4 +24,7 @@ enabled=true port=0 host=127.0.0.1 -extensions.routes=org.apache.james.webadmin.dropwizard.MetricsRoutes \ No newline at end of file +extensions.routes=org.apache.james.webadmin.dropwizard.MetricsRoutes + +# These tests exercise WebAdmin routes without authentication +password.generate=false diff --git a/server/protocols/webadmin-integration-test/postgres-webadmin-integration-test/src/test/resources/webadmin.properties b/server/protocols/webadmin-integration-test/postgres-webadmin-integration-test/src/test/resources/webadmin.properties index 78a176aabd..6602aa0f0a 100644 --- a/server/protocols/webadmin-integration-test/postgres-webadmin-integration-test/src/test/resources/webadmin.properties +++ b/server/protocols/webadmin-integration-test/postgres-webadmin-integration-test/src/test/resources/webadmin.properties @@ -24,4 +24,7 @@ enabled=true port=0 host=127.0.0.1 -extensions.routes=org.apache.james.webadmin.dropwizard.MetricsRoutes \ No newline at end of file +extensions.routes=org.apache.james.webadmin.dropwizard.MetricsRoutes + +# These tests exercise WebAdmin routes without authentication +password.generate=false diff --git a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/authentication/PasswordGenerator.java b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/authentication/PasswordGenerator.java new file mode 100644 index 0000000000..da032ba61f --- /dev/null +++ b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/authentication/PasswordGenerator.java @@ -0,0 +1,45 @@ +/**************************************************************** + * 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.webadmin.authentication; + +import java.security.SecureRandom; +import java.util.Base64; + +/** + * Generates ephemeral WebAdmin passwords, enabling a secure setup without hardcoded credentials. + * + * The base64 URL alphabet is used as it excludes the comma, which {@link PasswordFilter} relies on + * as a password separator. + */ +public class PasswordGenerator { + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + private static final Base64.Encoder ENCODER = Base64.getUrlEncoder().withoutPadding(); + private static final int ENTROPY_BYTE_COUNT = 32; + + public static String generate() { + byte[] entropy = new byte[ENTROPY_BYTE_COUNT]; + SECURE_RANDOM.nextBytes(entropy); + return ENCODER.encodeToString(entropy); + } + + private PasswordGenerator() { + + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
