This is an automated email from the ASF dual-hosted git repository.
chibenwa pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
The following commit(s) were added to refs/heads/master by this push:
new 0cd766633a JAMES-4201 Remove the `password.nodelete` webadmin access
level
0cd766633a is described below
commit 0cd766633a23a4125f1275f1691e6712087b3f5c
Author: Benoit TELLIER <[email protected]>
AuthorDate: Wed Sep 2 13:37:24 2026 +0200
JAMES-4201 Remove the `password.nodelete` webadmin access level
`password.nodelete` was introduced alongside `password.readonly` and never
released. It promised that such a credential "can read and create/modify
data
but cannot delete", but the filter only compares the literal HTTP method,
while
WebAdmin models plenty of destructive operations as POST or PUT:
- `POST /users/{username}?action=deleteData` and
`POST /domains/{domainName}?action=deleteData` erase user data through a
task,
- `PUT /users/{username}` resets the password of an existing account,
which is a
full account takeover,
- `PUT /domains/{domain}/aliases/{alias}` and the forward routes reroute
mail.
Making the filter action-aware would only paper over this: on an
administration
API where creating and updating is as destructive as deleting, "everything
but
the DELETE verb" cannot be a privilege separation, merely a guard-rail
against
mistyped `curl` commands - one that reads as a security boundary and is
documented as one.
So drop the level rather than keep a boundary we cannot honour.
---
CHANGELOG.md | 2 +-
.../servers/partials/configure/webadmin.adoc | 32 +++++-----------
.../sample-configuration/webadmin.properties | 4 --
.../sample-configuration/webadmin.properties | 4 --
.../sample-configuration/webadmin.properties | 4 --
.../sample-configuration/webadmin.properties | 4 --
.../sample-configuration/webadmin.properties | 4 --
.../sample-configuration/webadmin.properties | 4 --
.../sample-configuration/webadmin.properties | 4 --
.../james/modules/server/WebAdminServerModule.java | 5 +--
.../james/webadmin/WebAdminConfiguration.java | 25 ++-----------
.../webadmin/authentication/PasswordFilter.java | 12 +-----
.../james/webadmin/WebAdminConfigurationTest.java | 37 -------------------
.../authentication/PasswordFilterTest.java | 43 +---------------------
14 files changed, 17 insertions(+), 167 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2fddc16c72..743a141100 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,7 +15,7 @@ The format is based on [Keep a
Changelog](http://keepachangelog.com/en/1.0.0/)
### Security
- JAMES-4193 Correct BoringSSL TLS 1.3 cipher suite sanitizing that led to
disabled TLS 1.3 (#3029)
- - JAMES-4201 Implement granular password access control in webadmin
(`password.readonly` and `password.nodelete`)
+ - JAMES-4201 Implement a read-only password access level in webadmin
(`password.readonly`)
- 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
diff --git a/docs/modules/servers/partials/configure/webadmin.adoc
b/docs/modules/servers/partials/configure/webadmin.adoc
index c0f7f2a6da..5fb20c33a9 100644
--- a/docs/modules/servers/partials/configure/webadmin.adoc
+++ b/docs/modules/servers/partials/configure/webadmin.adoc
@@ -106,29 +106,15 @@ 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
-....
+Note that this is the only reduced privilege level WebAdmin offers: read-only,
or full administrative access.
+Any password configured through `password` grants the full WebAdmin surface,
which includes resetting the
+credentials of arbitrary users, rerouting their mail and deleting their data.
+
+WebAdmin has no notion of administrator identity, and models several
destructive operations as `POST` task
+actions rather than as `DELETE` requests, so no HTTP verb based scheme can
express a meaningful partial
+privilege. Deployments needing per-operation, per-domain or per-administrator
delegation should place an
+authenticating reverse proxy in front of WebAdmin, which can tie an
authenticated identity to the set of
+routes - query parameters included - it is allowed to call.
| jwt.enable
| Allow JSON Web Token as an authentication mechanism (default: false)
diff --git
a/server/apps/distributed-app/sample-configuration/webadmin.properties
b/server/apps/distributed-app/sample-configuration/webadmin.properties
index 27bc60395c..d8f1632021 100644
--- a/server/apps/distributed-app/sample-configuration/webadmin.properties
+++ b/server/apps/distributed-app/sample-configuration/webadmin.properties
@@ -68,7 +68,3 @@ password.generate=true
# 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 27bc60395c..d8f1632021 100644
--- a/server/apps/distributed-pop3-app/sample-configuration/webadmin.properties
+++ b/server/apps/distributed-pop3-app/sample-configuration/webadmin.properties
@@ -68,7 +68,3 @@ password.generate=true
# 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 c2a336b1e0..d280a9e544 100644
--- a/server/apps/jpa-app/sample-configuration/webadmin.properties
+++ b/server/apps/jpa-app/sample-configuration/webadmin.properties
@@ -62,7 +62,3 @@ password.generate=true
# 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 c2a336b1e0..d280a9e544 100644
--- a/server/apps/jpa-smtp-app/sample-configuration/webadmin.properties
+++ b/server/apps/jpa-smtp-app/sample-configuration/webadmin.properties
@@ -62,7 +62,3 @@ password.generate=true
# 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 a451aca7b8..1898d6c4b3 100644
--- a/server/apps/memory-app/sample-configuration/webadmin.properties
+++ b/server/apps/memory-app/sample-configuration/webadmin.properties
@@ -69,7 +69,3 @@ password.generate=true
# 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 c2a336b1e0..d280a9e544 100644
--- a/server/apps/postgres-app/sample-configuration/webadmin.properties
+++ b/server/apps/postgres-app/sample-configuration/webadmin.properties
@@ -62,7 +62,3 @@ password.generate=true
# 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 986133dfa5..e5ce733c35 100644
--- a/server/apps/scaling-pulsar-smtp/sample-configuration/webadmin.properties
+++ b/server/apps/scaling-pulsar-smtp/sample-configuration/webadmin.properties
@@ -62,7 +62,3 @@ password.generate=true
# 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/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 838b412417..e11bd08a01 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
@@ -172,7 +172,6 @@ public class WebAdminServerModule extends AbstractModule {
.minThreadCount(Optional.ofNullable(configurationFile.getInteger("minThreadCount",
null)))
.password(password(configurationFile, webAdminEnabled,
passwordGenerationDefault))
.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.");
@@ -241,8 +240,7 @@ public class WebAdminServerModule extends AbstractModule {
}
if (isPasswordPresent(webAdminConfiguration)) {
return new PasswordFilter(webAdminConfiguration.getPassword(),
- webAdminConfiguration.getReadOnlyPassword(),
- webAdminConfiguration.getNoDeletePassword());
+ webAdminConfiguration.getReadOnlyPassword());
}
return new NoAuthenticationFilter();
} catch (FileNotFoundException e) {
@@ -252,7 +250,6 @@ public class WebAdminServerModule extends AbstractModule {
private boolean isPasswordPresent(WebAdminConfiguration
webAdminConfiguration) {
return webAdminConfiguration.getPassword().isPresent()
- || webAdminConfiguration.getNoDeletePassword().isPresent()
|| webAdminConfiguration.getReadOnlyPassword().isPresent();
}
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 1a6bc158b6..706a9f10eb 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
@@ -59,7 +59,6 @@ public class WebAdminConfiguration {
private Optional<String> host = Optional.empty();
private Optional<String> password = Optional.empty();
private Optional<String> readOnlyPassword = Optional.empty();
- private Optional<String> noDeletePassword = Optional.empty();
private ImmutableList.Builder<String> additionalRoutes =
ImmutableList.builder();
private Optional<String> jwtPublicKey = Optional.empty();
private Optional<Integer> maxThreadCount = Optional.empty();
@@ -146,16 +145,6 @@ public class WebAdminConfiguration {
return this;
}
- public Builder noDeletePassword(String noDeletePassword) {
- this.noDeletePassword = Optional.ofNullable(noDeletePassword);
- return this;
- }
-
- public Builder noDeletePassword(Optional<String> noDeletePassword) {
- this.noDeletePassword = noDeletePassword;
- return this;
- }
-
public Builder additionalRoute(String additionalRoute) {
this.additionalRoutes.add(additionalRoute);
return this;
@@ -190,7 +179,6 @@ public class WebAdminConfiguration {
jwtPublicKey,
password,
readOnlyPassword,
- noDeletePassword,
maxThreadCount,
minThreadCount);
}
@@ -206,15 +194,14 @@ public class WebAdminConfiguration {
private final Optional<String> jwtPublicKey;
private final Optional<String> password;
private final Optional<String> readOnlyPassword;
- private final Optional<String> noDeletePassword;
private final Optional<Integer> maxThreadCount;
private final Optional<Integer> minThreadCount;
@VisibleForTesting
WebAdminConfiguration(boolean enabled, Optional<PortSupplier> port,
Optional<TlsConfiguration> tlsConfiguration,
boolean enableCORS, String urlCORSOrigin, String
host, List<String> additionalRoutes,
- Optional<String> jwtPublicKey, Optional<String>
password, Optional<String> readOnlyPassword,
- Optional<String> noDeletePassword, Optional<Integer>
maxThreadCount, Optional<Integer> minThreadCount) {
+ Optional<String> jwtPublicKey, Optional<String>
password, Optional<String> readOnlyPassword,
+ Optional<Integer> maxThreadCount, Optional<Integer>
minThreadCount) {
this.enabled = enabled;
this.port = port;
this.tlsConfiguration = tlsConfiguration;
@@ -225,7 +212,6 @@ public class WebAdminConfiguration {
this.jwtPublicKey = jwtPublicKey;
this.password = password;
this.readOnlyPassword = readOnlyPassword;
- this.noDeletePassword = noDeletePassword;
this.maxThreadCount = maxThreadCount;
this.minThreadCount = minThreadCount;
}
@@ -282,10 +268,6 @@ public class WebAdminConfiguration {
return readOnlyPassword;
}
- public Optional<String> getNoDeletePassword() {
- return noDeletePassword;
- }
-
@Override
public final boolean equals(Object o) {
if (o instanceof WebAdminConfiguration) {
@@ -300,7 +282,6 @@ public class WebAdminConfiguration {
&& 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);
@@ -311,6 +292,6 @@ public class WebAdminConfiguration {
@Override
public final int hashCode() {
return Objects.hash(enabled, port, tlsConfiguration, enableCORS,
jwtPublicKey, urlCORSOrigin, host,
- password, readOnlyPassword, noDeletePassword, additionalRoutes,
minThreadCount, maxThreadCount);
+ password, readOnlyPassword, additionalRoutes, minThreadCount,
maxThreadCount);
}
}
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 6c46db42ab..bba959fe99 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
@@ -42,22 +42,18 @@ public class PasswordFilter implements AuthenticationFilter
{
private static final String GET_METHOD = "GET";
private static final String HEAD_METHOD = "HEAD";
- private static final String DELETE_METHOD = "DELETE";
private final Optional<List<String>> passwords;
private final Optional<List<String>> readOnlyPasswords;
- private final Optional<List<String>> 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(Optional<String> passwordString, Optional<String>
readOnlyPasswordString, Optional<String> noDeletePasswordString) {
+ public PasswordFilter(Optional<String> passwordString, Optional<String>
readOnlyPasswordString) {
this.passwords = splitOptionalPasswords(passwordString);
this.readOnlyPasswords =
splitOptionalPasswords(readOnlyPasswordString);
- this.noDeletePasswords =
splitOptionalPasswords(noDeletePasswordString);
}
private Optional<List<String>> splitOptionalPasswords(Optional<String>
optionalPasswordString) {
@@ -73,7 +69,6 @@ public class PasswordFilter implements AuthenticationFilter {
private enum AccessLevel {
FULL,
- NO_DELETE,
READ_ONLY,
NONE
}
@@ -82,9 +77,6 @@ public class PasswordFilter implements AuthenticationFilter {
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;
}
@@ -95,8 +87,6 @@ public class PasswordFilter implements AuthenticationFilter {
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:
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 b8736c9061..9345df4058 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
@@ -229,43 +229,6 @@ class WebAdminConfigurationTest {
.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<String> 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();
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 b2295b44df..26f7c59b18 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
@@ -40,7 +40,7 @@ class PasswordFilterTest {
@BeforeEach
void setUp() {
- testee = new PasswordFilter(Optional.of("abc,def"),
Optional.of("readonly1,readonly2"), Optional.of("nodelete1,nodelete2"));
+ testee = new PasswordFilter(Optional.of("abc,def"),
Optional.of("readonly1,readonly2"));
}
@Test
@@ -172,45 +172,6 @@ class PasswordFilterTest {
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);
@@ -231,7 +192,7 @@ class PasswordFilterTest {
@Test
void handleShouldRejectWhenNoConfiguredPasswords() {
- PasswordFilter filterWithNulls = new PasswordFilter(Optional.empty(),
Optional.empty(), Optional.empty());
+ PasswordFilter filterWithNulls = new PasswordFilter(Optional.empty(),
Optional.empty());
Request request = mock(Request.class);
when(request.requestMethod()).thenReturn("GET");
when(request.headers("Password")).thenReturn("anypassword");
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]