From 40875e4cb1984bb87c763432cf366892256f4d1b Mon Sep 17 00:00:00 2001 From: Rene Cordier Date: Fri, 10 Apr 2026 15:05:11 +0700 Subject: [PATCH 1/3] JAMES-4201 Add password.readonly and password.nodelete options to webadmin configuration --- .../modules/server/WebAdminServerModule.java | 2 + .../james/webadmin/WebAdminConfiguration.java | 45 ++++++++++- .../webadmin/WebAdminConfigurationTest.java | 76 +++++++++++++++++++ 3 files changed, 121 insertions(+), 2 deletions(-) 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 9b9dcca5614..bf42c4f701a 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 @@ -156,6 +156,8 @@ public WebAdminConfiguration provideWebAdminConfiguration(FileSystem fileSystem, .maxThreadCount(Optional.ofNullable(configurationFile.getInteger("maxThreadCount", null))) .minThreadCount(Optional.ofNullable(configurationFile.getInteger("minThreadCount", null))) .password(Optional.ofNullable(configurationFile.getString("password", null))) + .readOnlyPassword(Optional.ofNullable(configurationFile.getString("password.readonly", null))) + .noDeletePassword(Optional.ofNullable(configurationFile.getString("password.nodelete", null))) .build(); } catch (FileNotFoundException e) { LOGGER.info("No webadmin.properties file. Disabling WebAdmin interface."); diff --git a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/WebAdminConfiguration.java b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/WebAdminConfiguration.java index 820a8cc826e..1a6bc158b67 100644 --- a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/WebAdminConfiguration.java +++ b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/WebAdminConfiguration.java @@ -58,6 +58,8 @@ public static class Builder { private Optional urlCORSOrigin = Optional.empty(); private Optional host = Optional.empty(); private Optional password = Optional.empty(); + private Optional readOnlyPassword = Optional.empty(); + private Optional noDeletePassword = Optional.empty(); private ImmutableList.Builder additionalRoutes = ImmutableList.builder(); private Optional jwtPublicKey = Optional.empty(); private Optional maxThreadCount = Optional.empty(); @@ -134,6 +136,26 @@ public Builder password(Optional password) { return this; } + public Builder readOnlyPassword(String readOnlyPassword) { + this.readOnlyPassword = Optional.ofNullable(readOnlyPassword); + return this; + } + + public Builder readOnlyPassword(Optional readOnlyPassword) { + this.readOnlyPassword = readOnlyPassword; + return this; + } + + public Builder noDeletePassword(String noDeletePassword) { + this.noDeletePassword = Optional.ofNullable(noDeletePassword); + return this; + } + + public Builder noDeletePassword(Optional noDeletePassword) { + this.noDeletePassword = noDeletePassword; + return this; + } + public Builder additionalRoute(String additionalRoute) { this.additionalRoutes.add(additionalRoute); return this; @@ -167,6 +189,8 @@ public WebAdminConfiguration build() { additionalRoutes.build(), jwtPublicKey, password, + readOnlyPassword, + noDeletePassword, maxThreadCount, minThreadCount); } @@ -181,12 +205,16 @@ public WebAdminConfiguration build() { private final List additionalRoutes; private final Optional jwtPublicKey; private final Optional password; + private final Optional readOnlyPassword; + private final Optional noDeletePassword; private final Optional maxThreadCount; private final Optional minThreadCount; @VisibleForTesting WebAdminConfiguration(boolean enabled, Optional port, Optional tlsConfiguration, - boolean enableCORS, String urlCORSOrigin, String host, List additionalRoutes, Optional jwtPublicKey, Optional password, Optional maxThreadCount, Optional minThreadCount) { + boolean enableCORS, String urlCORSOrigin, String host, List additionalRoutes, + Optional jwtPublicKey, Optional password, Optional readOnlyPassword, + Optional noDeletePassword, Optional maxThreadCount, Optional minThreadCount) { this.enabled = enabled; this.port = port; this.tlsConfiguration = tlsConfiguration; @@ -196,6 +224,8 @@ public WebAdminConfiguration build() { this.additionalRoutes = additionalRoutes; this.jwtPublicKey = jwtPublicKey; this.password = password; + this.readOnlyPassword = readOnlyPassword; + this.noDeletePassword = noDeletePassword; this.maxThreadCount = maxThreadCount; this.minThreadCount = minThreadCount; } @@ -248,6 +278,14 @@ public Optional getPassword() { return password; } + public Optional getReadOnlyPassword() { + return readOnlyPassword; + } + + public Optional getNoDeletePassword() { + return noDeletePassword; + } + @Override public final boolean equals(Object o) { if (o instanceof WebAdminConfiguration) { @@ -261,6 +299,8 @@ public final boolean equals(Object o) { && Objects.equals(this.urlCORSOrigin, that.urlCORSOrigin) && Objects.equals(this.host, that.host) && Objects.equals(this.password, that.password) + && Objects.equals(this.readOnlyPassword, that.readOnlyPassword) + && Objects.equals(this.noDeletePassword, that.noDeletePassword) && Objects.equals(this.additionalRoutes, that.additionalRoutes) && Objects.equals(this.minThreadCount, that.minThreadCount) && Objects.equals(this.maxThreadCount, that.maxThreadCount); @@ -270,6 +310,7 @@ public final boolean equals(Object o) { @Override public final int hashCode() { - return Objects.hash(enabled, port, tlsConfiguration, enableCORS, jwtPublicKey, urlCORSOrigin, host, additionalRoutes, minThreadCount, maxThreadCount, password); + return Objects.hash(enabled, port, tlsConfiguration, enableCORS, jwtPublicKey, urlCORSOrigin, host, + password, readOnlyPassword, noDeletePassword, additionalRoutes, minThreadCount, maxThreadCount); } } diff --git a/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/WebAdminConfigurationTest.java b/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/WebAdminConfigurationTest.java index 3b390f20fee..b8736c90616 100644 --- a/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/WebAdminConfigurationTest.java +++ b/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/WebAdminConfigurationTest.java @@ -22,6 +22,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.Optional; + import org.junit.jupiter.api.Test; import nl.jqno.equalsverifier.EqualsVerifier; @@ -190,6 +192,80 @@ void builderShouldDefineHostWithSetting() { .isEqualTo(host); } + @Test + void builderShouldAcceptReadOnlyPassword() { + String password = "readonly123"; + assertThat( + WebAdminConfiguration.builder() + .enabled() + .port(PORT) + .readOnlyPassword(password) + .build()) + .extracting(WebAdminConfiguration::getReadOnlyPassword) + .isEqualTo(Optional.of(password)); + } + + @Test + void builderShouldAcceptReadOnlyPasswordAsOptional() { + Optional password = Optional.of("readonly456"); + assertThat( + WebAdminConfiguration.builder() + .enabled() + .port(PORT) + .readOnlyPassword(password) + .build()) + .extracting(WebAdminConfiguration::getReadOnlyPassword) + .isEqualTo(password); + } + + @Test + void builderShouldReturnEmptyReadOnlyPasswordByDefault() { + assertThat( + WebAdminConfiguration.builder() + .enabled() + .port(PORT) + .build()) + .extracting(WebAdminConfiguration::getReadOnlyPassword) + .isEqualTo(Optional.empty()); + } + + @Test + void builderShouldAcceptNoDeletePassword() { + String password = "nodelete123"; + assertThat( + WebAdminConfiguration.builder() + .enabled() + .port(PORT) + .noDeletePassword(password) + .build()) + .extracting(WebAdminConfiguration::getNoDeletePassword) + .isEqualTo(Optional.of(password)); + } + + @Test + void builderShouldAcceptNoDeletePasswordAsOptional() { + Optional password = Optional.of("nodelete456"); + assertThat( + WebAdminConfiguration.builder() + .enabled() + .port(PORT) + .noDeletePassword(password) + .build()) + .extracting(WebAdminConfiguration::getNoDeletePassword) + .isEqualTo(password); + } + + @Test + void builderShouldReturnEmptyNoDeletePasswordByDefault() { + assertThat( + WebAdminConfiguration.builder() + .enabled() + .port(PORT) + .build()) + .extracting(WebAdminConfiguration::getNoDeletePassword) + .isEqualTo(Optional.empty()); + } + @Test void shouldMatchBeanContract() { EqualsVerifier.forClass(WebAdminConfiguration.class).verify(); From a362b78a0dc214b25c5d021197bba56e66edb25f Mon Sep 17 00:00:00 2001 From: Rene Cordier Date: Fri, 10 Apr 2026 15:29:17 +0700 Subject: [PATCH 2/3] JAMES-4201 Implement granular password access control in PasswordFilter --- .../modules/server/WebAdminServerModule.java | 15 ++- .../authentication/PasswordFilter.java | 100 +++++++++++--- .../authentication/PasswordFilterTest.java | 127 +++++++++++++++++- 3 files changed, 216 insertions(+), 26 deletions(-) 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 bf42c4f701a..000e6c99010 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 @@ -193,14 +193,23 @@ public AuthenticationFilter providesAuthenticationFilter(PropertiesProvider prop if (configurationFile.getBoolean("jwt.enabled", DEFAULT_JWT_DISABLED)) { return new JwtFilter(jwtTokenVerifier); } - return webAdminConfiguration.getPassword() - .map(PasswordFilter::new) - .orElse(new NoAuthenticationFilter()); + if (isPasswordPresent(webAdminConfiguration)) { + return new PasswordFilter(webAdminConfiguration.getPassword(), + webAdminConfiguration.getReadOnlyPassword(), + webAdminConfiguration.getNoDeletePassword()); + } + return new NoAuthenticationFilter(); } catch (FileNotFoundException e) { return new NoAuthenticationFilter(); } } + private boolean isPasswordPresent(WebAdminConfiguration webAdminConfiguration) { + return webAdminConfiguration.getPassword().isPresent() + || webAdminConfiguration.getNoDeletePassword().isPresent() + || webAdminConfiguration.getReadOnlyPassword().isPresent(); + } + @Provides @Singleton @Named("webadmin") diff --git a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/authentication/PasswordFilter.java b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/authentication/PasswordFilter.java index e324f759608..6c46db42ab5 100644 --- a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/authentication/PasswordFilter.java +++ b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/authentication/PasswordFilter.java @@ -29,6 +29,7 @@ import org.eclipse.jetty.http.HttpStatus; import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; import spark.Request; import spark.Response; @@ -39,12 +40,69 @@ public class PasswordFilter implements AuthenticationFilter { public static final String AUTHORIZATION_HEADER_PREFIX = "Bearer "; public static final String AUTHORIZATION_HEADER_NAME = "Authorization"; - private final List passwords; + private static final String GET_METHOD = "GET"; + private static final String HEAD_METHOD = "HEAD"; + private static final String DELETE_METHOD = "DELETE"; + private final Optional> passwords; + private final Optional> readOnlyPasswords; + private final Optional> noDeletePasswords; + + /** + * @param passwordString optional comma-separated list of full-access passwords + * @param readOnlyPasswordString optional comma-separated list of read-only passwords + * @param noDeletePasswordString optional comma-separated list of no-delete passwords + */ @Inject - public PasswordFilter(String passwordString) { - this.passwords = Splitter.on(',') - .splitToList(passwordString); + public PasswordFilter(Optional passwordString, Optional readOnlyPasswordString, Optional noDeletePasswordString) { + this.passwords = splitOptionalPasswords(passwordString); + this.readOnlyPasswords = splitOptionalPasswords(readOnlyPasswordString); + this.noDeletePasswords = splitOptionalPasswords(noDeletePasswordString); + } + + private Optional> splitOptionalPasswords(Optional optionalPasswordString) { + return optionalPasswordString.map(this::splitPasswords); + } + + private List splitPasswords(String passwordString) { + if (passwordString == null || passwordString.isEmpty()) { + return ImmutableList.of(); + } + return Splitter.on(',').splitToList(passwordString); + } + + private enum AccessLevel { + FULL, + NO_DELETE, + READ_ONLY, + NONE + } + + private AccessLevel getAccessLevel(String password) { + if (passwords.isPresent() && passwords.get().contains(password)) { + return AccessLevel.FULL; + } + if (noDeletePasswords.isPresent() && noDeletePasswords.get().contains(password)) { + return AccessLevel.NO_DELETE; + } + if (readOnlyPasswords.isPresent() && readOnlyPasswords.get().contains(password)) { + return AccessLevel.READ_ONLY; + } + return AccessLevel.NONE; + } + + private boolean isAccessAllowed(AccessLevel accessLevel, String httpMethod) { + switch (accessLevel) { + case FULL: + return true; + case NO_DELETE: + return !httpMethod.equals(DELETE_METHOD); + case READ_ONLY: + return httpMethod.equals(GET_METHOD) || httpMethod.equals(HEAD_METHOD); + case NONE: + default: + return false; + } } @Override @@ -53,22 +111,24 @@ public void handle(Request request, Response response) throws Exception { Optional password = Optional.ofNullable(request.headers(PASSWORD)); Optional authorization = Optional.ofNullable(request.headers(AUTHORIZATION_HEADER_NAME)); - if (!password.isPresent()) { - if (authorization.isPresent()) { - Optional bearer = authorization - .filter(value -> value.startsWith(AUTHORIZATION_HEADER_PREFIX)) - .map(value -> value.substring(AUTHORIZATION_HEADER_PREFIX.length())); - - if (!bearer.filter(passwords::contains).isPresent()) { - halt(HttpStatus.UNAUTHORIZED_401, "Wrong Bearer."); - } - } else { - halt(HttpStatus.UNAUTHORIZED_401, "No Password header."); - } - } else { - if (!passwords.contains(password.get())) { - halt(HttpStatus.UNAUTHORIZED_401, "Wrong Password header."); - } + Optional providedPassword = password + .or(() -> authorization + .filter(value -> value.startsWith(AUTHORIZATION_HEADER_PREFIX)) + .map(value -> value.substring(AUTHORIZATION_HEADER_PREFIX.length()))); + + if (providedPassword.isEmpty()) { + halt(HttpStatus.UNAUTHORIZED_401, "No Password in header."); + return; + } + + AccessLevel accessLevel = getAccessLevel(providedPassword.get()); + if (accessLevel == AccessLevel.NONE) { + halt(HttpStatus.UNAUTHORIZED_401, "Wrong password."); + return; + } + + if (!isAccessAllowed(accessLevel, request.requestMethod())) { + halt(HttpStatus.FORBIDDEN_403, "Insufficient permissions for this operation."); } } } diff --git a/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/authentication/PasswordFilterTest.java b/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/authentication/PasswordFilterTest.java index 29667c7deeb..b2295b44df0 100644 --- a/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/authentication/PasswordFilterTest.java +++ b/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/authentication/PasswordFilterTest.java @@ -24,6 +24,8 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import java.util.Optional; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -38,7 +40,7 @@ class PasswordFilterTest { @BeforeEach void setUp() { - testee = new PasswordFilter("abc,def"); + testee = new PasswordFilter(Optional.of("abc,def"), Optional.of("readonly1,readonly2"), Optional.of("nodelete1,nodelete2")); } @Test @@ -105,7 +107,6 @@ void handleShouldRejectBothPassword() { void handleShouldAcceptValidPassword() throws Exception { Request request = mock(Request.class); when(request.requestMethod()).thenReturn("GET"); - when(request.requestMethod()).thenReturn("GET"); when(request.headers("Password")).thenReturn("abc"); testee.handle(request, mock(Response.class)); @@ -115,9 +116,129 @@ void handleShouldAcceptValidPassword() throws Exception { void handleShouldAcceptValidPasswordWhenBearer() throws Exception { Request request = mock(Request.class); when(request.requestMethod()).thenReturn("GET"); - when(request.requestMethod()).thenReturn("GET"); when(request.headers("Authorization")).thenReturn("Bearer abc"); testee.handle(request, mock(Response.class)); } + + @Test + void handleShouldAcceptValidReadOnlyPasswordOnGet() throws Exception { + Request request = mock(Request.class); + when(request.requestMethod()).thenReturn("GET"); + when(request.headers("Password")).thenReturn("readonly1"); + + testee.handle(request, mock(Response.class)); + } + + @Test + void handleShouldRejectReadOnlyPasswordOnPost() { + Request request = mock(Request.class); + when(request.requestMethod()).thenReturn("POST"); + when(request.headers("Password")).thenReturn("readonly1"); + + assertThatThrownBy(() -> testee.handle(request, mock(Response.class))) + .isInstanceOf(HaltException.class) + .extracting(e -> HaltException.class.cast(e).statusCode()) + .isEqualTo(403); + } + + @Test + void handleShouldRejectReadOnlyPasswordOnDelete() { + Request request = mock(Request.class); + when(request.requestMethod()).thenReturn("DELETE"); + when(request.headers("Password")).thenReturn("readonly1"); + + assertThatThrownBy(() -> testee.handle(request, mock(Response.class))) + .isInstanceOf(HaltException.class) + .extracting(e -> HaltException.class.cast(e).statusCode()) + .isEqualTo(403); + } + + @Test + void handleShouldAcceptValidReadOnlyPasswordOnGetViaBearer() throws Exception { + Request request = mock(Request.class); + when(request.requestMethod()).thenReturn("GET"); + when(request.headers("Authorization")).thenReturn("Bearer readonly2"); + + testee.handle(request, mock(Response.class)); + } + + @Test + void handleShouldAcceptValidReadOnlyPasswordOnHeadViaBearer() throws Exception { + Request request = mock(Request.class); + when(request.requestMethod()).thenReturn("HEAD"); + when(request.headers("Authorization")).thenReturn("Bearer readonly2"); + + testee.handle(request, mock(Response.class)); + } + + @Test + void handleShouldAcceptValidNoDeletePasswordOnGet() throws Exception { + Request request = mock(Request.class); + when(request.requestMethod()).thenReturn("GET"); + when(request.headers("Password")).thenReturn("nodelete1"); + + testee.handle(request, mock(Response.class)); + } + + @Test + void handleShouldAcceptValidNoDeletePasswordOnPost() throws Exception { + Request request = mock(Request.class); + when(request.requestMethod()).thenReturn("POST"); + when(request.headers("Password")).thenReturn("nodelete1"); + + testee.handle(request, mock(Response.class)); + } + + @Test + void handleShouldRejectNoDeletePasswordOnDelete() { + Request request = mock(Request.class); + when(request.requestMethod()).thenReturn("DELETE"); + when(request.headers("Password")).thenReturn("nodelete1"); + + assertThatThrownBy(() -> testee.handle(request, mock(Response.class))) + .isInstanceOf(HaltException.class) + .extracting(e -> HaltException.class.cast(e).statusCode()) + .isEqualTo(403); + } + + @Test + void handleShouldAcceptValidNoDeletePasswordOnPut() throws Exception { + Request request = mock(Request.class); + when(request.requestMethod()).thenReturn("PUT"); + when(request.headers("Password")).thenReturn("nodelete2"); + + testee.handle(request, mock(Response.class)); + } + + @Test + void handleShouldAcceptFullPasswordOnDelete() throws Exception { + Request request = mock(Request.class); + when(request.requestMethod()).thenReturn("DELETE"); + when(request.headers("Password")).thenReturn("abc"); + + testee.handle(request, mock(Response.class)); + } + + @Test + void handleShouldAcceptFullPasswordOnPost() throws Exception { + Request request = mock(Request.class); + when(request.requestMethod()).thenReturn("POST"); + when(request.headers("Password")).thenReturn("def"); + + testee.handle(request, mock(Response.class)); + } + + @Test + void handleShouldRejectWhenNoConfiguredPasswords() { + PasswordFilter filterWithNulls = new PasswordFilter(Optional.empty(), Optional.empty(), Optional.empty()); + Request request = mock(Request.class); + when(request.requestMethod()).thenReturn("GET"); + when(request.headers("Password")).thenReturn("anypassword"); + + assertThatThrownBy(() -> filterWithNulls.handle(request, mock(Response.class))) + .isInstanceOf(HaltException.class) + .extracting(e -> HaltException.class.cast(e).statusCode()) + .isEqualTo(401); + } } From 0dd2ee7b4dbc2456e60bf8cf2ee028c2c2206c8b Mon Sep 17 00:00:00 2001 From: Rene Cordier Date: Fri, 10 Apr 2026 15:32:24 +0700 Subject: [PATCH 3/3] JAMES-4201 Webadmin documentation for password.readonly and password.nodelete --- .../servers/partials/configure/webadmin.adoc | 64 ++++++++++++++++++- .../sample-configuration/webadmin.properties | 14 +++- .../sample-configuration/webadmin.properties | 14 +++- .../sample-configuration/webadmin.properties | 14 +++- .../sample-configuration/webadmin.properties | 14 +++- .../sample-configuration/webadmin.properties | 14 +++- .../sample-configuration/webadmin.properties | 14 +++- .../sample-configuration/webadmin.properties | 16 ++++- 8 files changed, 153 insertions(+), 11 deletions(-) diff --git a/docs/modules/servers/partials/configure/webadmin.adoc b/docs/modules/servers/partials/configure/webadmin.adoc index 819691de2d2..e2566a59c3e 100644 --- a/docs/modules/servers/partials/configure/webadmin.adoc +++ b/docs/modules/servers/partials/configure/webadmin.adoc @@ -8,8 +8,8 @@ The web administration supports for now the CRUD operations on: - Performing cassandra migrations [small]*_(only for Distributed James Server that uses cassandra as backend)_* - And much more, as described in the following sections. -*WARNING*: This API allows authentication only via the use of JWT. If not -configured with JWT, an administrator should ensure an attacker can not +*WARNING*: This API supports authentication via JWT or static passwords. If not +configured with either, an administrator should ensure an attacker cannot use this API. By the way, some endpoints are not filtered by authentication. Those endpoints are not related to data stored in James, @@ -41,7 +41,7 @@ to get some examples and hints. | password | Uses a configured static value for authentication. It relies on the Password header. -It supports several passwords, configured as a coma separated list. +It supports several passwords, configured as a comma separated list. .... password=secretA,secretB,secretC @@ -62,6 +62,54 @@ Password: secretD As well as request without the password header. +| password.readonly +| Configures passwords that only allow read operations (GET requests). +These passwords can view data but cannot modify it. +Multiple passwords can be configured as a comma-separated list. + +.... +password.readonly=aaa,bbb +.... + +Requests with these passwords will be allowed for: + +.... +GET /domains +GET /users +.... + +But denied for: + +.... +POST /domains +DELETE /users +PUT /mailboxes +.... + +| password.nodelete +| Configures passwords that allow all operations except DELETE. +These passwords can read and create/modify data but cannot delete. +Multiple passwords can be configured as a comma-separated list. + +.... +password.nodelete=ccc,ddd +.... + +Requests with these passwords will be allowed for: + +.... +GET /domains +POST /users +PUT /mailboxes +.... ++ +But denied for: ++ +.... +DELETE /domains +DELETE /users +.... + | jwt.enable | Allow JSON Web Token as an authentication mechanism (default: false) @@ -98,6 +146,16 @@ xref:{pages-path}/customization/webadmin-routes.adoc[creating you own webadmin r |=== +*Note on HTTP status codes:* + +- `401 Unauthorized` is returned when: + - No password header is provided + - The password is not recognized (not in any of the configured lists) + +- `403 Forbidden` is returned when: + - The password is valid but the HTTP method is not allowed for that password type + - For example, using a readonly password for a POST request + == Generating a JWT key pair The {server-name} enforces the use of RSA-SHA-256. diff --git a/server/apps/distributed-app/sample-configuration/webadmin.properties b/server/apps/distributed-app/sample-configuration/webadmin.properties index b97c71829c4..7376656fce0 100644 --- a/server/apps/distributed-app/sample-configuration/webadmin.properties +++ b/server/apps/distributed-app/sample-configuration/webadmin.properties @@ -52,4 +52,16 @@ 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 +# Configure one or more passwords (comma separated) for WebAdmin authentication +#password=secret1,secret2 + +# Read-only passwords - only allow GET requests +# These passwords can only perform read operations +#password.readonly=aaa,bbb + +# No-delete passwords - allow all operations except DELETE +# These passwords can perform read and write operations but not delete +#password.nodelete=ccc,ddd \ No newline at end of file diff --git a/server/apps/distributed-pop3-app/sample-configuration/webadmin.properties b/server/apps/distributed-pop3-app/sample-configuration/webadmin.properties index b97c71829c4..7376656fce0 100644 --- a/server/apps/distributed-pop3-app/sample-configuration/webadmin.properties +++ b/server/apps/distributed-pop3-app/sample-configuration/webadmin.properties @@ -52,4 +52,16 @@ 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 +# Configure one or more passwords (comma separated) for WebAdmin authentication +#password=secret1,secret2 + +# Read-only passwords - only allow GET requests +# These passwords can only perform read operations +#password.readonly=aaa,bbb + +# No-delete passwords - allow all operations except DELETE +# These passwords can perform read and write operations but not delete +#password.nodelete=ccc,ddd \ No newline at end of file diff --git a/server/apps/jpa-app/sample-configuration/webadmin.properties b/server/apps/jpa-app/sample-configuration/webadmin.properties index 5dc74740c55..e21c89b0bc3 100644 --- a/server/apps/jpa-app/sample-configuration/webadmin.properties +++ b/server/apps/jpa-app/sample-configuration/webadmin.properties @@ -46,4 +46,16 @@ 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 +# Configure one or more passwords (comma separated) for WebAdmin authentication +#password=secret1,secret2 + +# Read-only passwords - only allow GET requests +# These passwords can only perform read operations +#password.readonly=aaa,bbb + +# No-delete passwords - allow all operations except DELETE +# These passwords can perform read and write operations but not delete +#password.nodelete=ccc,ddd \ No newline at end of file diff --git a/server/apps/jpa-smtp-app/sample-configuration/webadmin.properties b/server/apps/jpa-smtp-app/sample-configuration/webadmin.properties index 5dc74740c55..e21c89b0bc3 100644 --- a/server/apps/jpa-smtp-app/sample-configuration/webadmin.properties +++ b/server/apps/jpa-smtp-app/sample-configuration/webadmin.properties @@ -46,4 +46,16 @@ 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 +# Configure one or more passwords (comma separated) for WebAdmin authentication +#password=secret1,secret2 + +# Read-only passwords - only allow GET requests +# These passwords can only perform read operations +#password.readonly=aaa,bbb + +# No-delete passwords - allow all operations except DELETE +# These passwords can perform read and write operations but not delete +#password.nodelete=ccc,ddd \ No newline at end of file diff --git a/server/apps/memory-app/sample-configuration/webadmin.properties b/server/apps/memory-app/sample-configuration/webadmin.properties index 3449eedc927..a9eb36a8136 100644 --- a/server/apps/memory-app/sample-configuration/webadmin.properties +++ b/server/apps/memory-app/sample-configuration/webadmin.properties @@ -53,4 +53,16 @@ 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 +# Configure one or more passwords (comma separated) for WebAdmin authentication +#password=secret1,secret2 + +# Read-only passwords - only allow GET requests +# These passwords can only perform read operations +#password.readonly=aaa,bbb + +# No-delete passwords - allow all operations except DELETE +# These passwords can perform read and write operations but not delete +#password.nodelete=ccc,ddd \ No newline at end of file diff --git a/server/apps/postgres-app/sample-configuration/webadmin.properties b/server/apps/postgres-app/sample-configuration/webadmin.properties index 5dc74740c55..e21c89b0bc3 100644 --- a/server/apps/postgres-app/sample-configuration/webadmin.properties +++ b/server/apps/postgres-app/sample-configuration/webadmin.properties @@ -46,4 +46,16 @@ 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 +# Configure one or more passwords (comma separated) for WebAdmin authentication +#password=secret1,secret2 + +# Read-only passwords - only allow GET requests +# These passwords can only perform read operations +#password.readonly=aaa,bbb + +# No-delete passwords - allow all operations except DELETE +# These passwords can perform read and write operations but not delete +#password.nodelete=ccc,ddd \ No newline at end of file diff --git a/server/apps/scaling-pulsar-smtp/sample-configuration/webadmin.properties b/server/apps/scaling-pulsar-smtp/sample-configuration/webadmin.properties index d042dd1525a..5a494e0e599 100644 --- a/server/apps/scaling-pulsar-smtp/sample-configuration/webadmin.properties +++ b/server/apps/scaling-pulsar-smtp/sample-configuration/webadmin.properties @@ -1,4 +1,4 @@ -m# Licensed to the Apache Software Foundation (ASF) under one +# 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 @@ -46,4 +46,16 @@ 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 +# Configure one or more passwords (comma separated) for WebAdmin authentication +#password=secret1,secret2 + +# Read-only passwords - only allow GET requests +# These passwords can only perform read operations +#password.readonly=aaa,bbb + +# No-delete passwords - allow all operations except DELETE +# These passwords can perform read and write operations but not delete +#password.nodelete=ccc,ddd \ No newline at end of file