This is an automated email from the ASF dual-hosted git repository.
rcordier 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 3629f5a2b0 JAMES-4190 Allow registering read only IMAP annotations
(#2975)
3629f5a2b0 is described below
commit 3629f5a2b0f5abece2cd78d2978ea3c90ea1c27f
Author: Benoit TELLIER <[email protected]>
AuthorDate: Mon Mar 16 03:53:05 2026 +0100
JAMES-4190 Allow registering read only IMAP annotations (#2975)
---
.../james/mailbox/ReadOnlyAnnotationPredicate.java | 37 +++++++++++++++++++
.../resources/META-INF/spring/spring-mailbox.xml | 6 +++
.../AggregatedReadOnlyAnnotationPredicate.java | 42 +++++++++++++++++++++
.../store/StoreMailboxAnnotationManager.java | 25 ++++++++++++-
.../store/StoreMailboxManagerAnnotationTest.java | 39 +++++++++++++++++++-
.../imap/processor/SetMetadataProcessorTest.java | 14 +++++++
.../org/apache/james/modules/MailboxModule.java | 2 +
.../AnnotationModule.java} | 43 ++++++++++------------
8 files changed, 181 insertions(+), 27 deletions(-)
diff --git
a/mailbox/api/src/main/java/org/apache/james/mailbox/ReadOnlyAnnotationPredicate.java
b/mailbox/api/src/main/java/org/apache/james/mailbox/ReadOnlyAnnotationPredicate.java
new file mode 100644
index 0000000000..b6952ea4f2
--- /dev/null
+++
b/mailbox/api/src/main/java/org/apache/james/mailbox/ReadOnlyAnnotationPredicate.java
@@ -0,0 +1,37 @@
+/****************************************************************
+ * 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.mailbox;
+
+import org.apache.james.mailbox.model.MailboxAnnotationKey;
+
+/**
+ * Extension point allowing modules to declare annotation keys that are
+ * server-computed and must not be modified by clients via SETMETADATA.
+ *
+ * Implementations are collected via Guice multibinding and aggregated by
+ * {@link
org.apache.james.mailbox.store.AggregatedReadOnlyAnnotationPredicate}.
+ */
+@FunctionalInterface
+public interface ReadOnlyAnnotationPredicate {
+
+ ReadOnlyAnnotationPredicate ALLOW_ALL = key -> false;
+
+ boolean isReadOnly(MailboxAnnotationKey key);
+}
diff --git
a/mailbox/spring/src/main/resources/META-INF/spring/spring-mailbox.xml
b/mailbox/spring/src/main/resources/META-INF/spring/spring-mailbox.xml
index 1c69f8705e..d342629c3a 100644
--- a/mailbox/spring/src/main/resources/META-INF/spring/spring-mailbox.xml
+++ b/mailbox/spring/src/main/resources/META-INF/spring/spring-mailbox.xml
@@ -47,9 +47,15 @@
-->
<bean id="messageParser"
class="org.apache.james.mailbox.store.mail.model.impl.MessageParserImpl"/>
+ <bean id="allowAllReadOnlyAnnotationPredicate"
+
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
+ <property name="staticField"
value="org.apache.james.mailbox.ReadOnlyAnnotationPredicate.ALLOW_ALL"/>
+ </bean>
+
<bean id="storeMailboxAnnotationManager"
class="org.apache.james.mailbox.store.StoreMailboxAnnotationManager">
<constructor-arg index="0" ref="messageMapperFactory" />
<constructor-arg index="1" ref="storeRightManager" />
+ <constructor-arg index="2" ref="allowAllReadOnlyAnnotationPredicate" />
</bean>
<bean id="storeRightManager"
class="org.apache.james.mailbox.store.StoreRightManager" >
diff --git
a/mailbox/store/src/main/java/org/apache/james/mailbox/store/AggregatedReadOnlyAnnotationPredicate.java
b/mailbox/store/src/main/java/org/apache/james/mailbox/store/AggregatedReadOnlyAnnotationPredicate.java
new file mode 100644
index 0000000000..26284682ba
--- /dev/null
+++
b/mailbox/store/src/main/java/org/apache/james/mailbox/store/AggregatedReadOnlyAnnotationPredicate.java
@@ -0,0 +1,42 @@
+/****************************************************************
+ * 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.mailbox.store;
+
+import java.util.Set;
+
+import jakarta.inject.Inject;
+
+import org.apache.james.mailbox.ReadOnlyAnnotationPredicate;
+import org.apache.james.mailbox.model.MailboxAnnotationKey;
+
+public class AggregatedReadOnlyAnnotationPredicate implements
ReadOnlyAnnotationPredicate {
+
+ private final Set<ReadOnlyAnnotationPredicate> predicates;
+
+ @Inject
+ public
AggregatedReadOnlyAnnotationPredicate(Set<ReadOnlyAnnotationPredicate>
predicates) {
+ this.predicates = predicates;
+ }
+
+ @Override
+ public boolean isReadOnly(MailboxAnnotationKey key) {
+ return predicates.stream().anyMatch(predicate ->
predicate.isReadOnly(key));
+ }
+}
diff --git
a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxAnnotationManager.java
b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxAnnotationManager.java
index 544043c265..89a38e71dc 100644
---
a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxAnnotationManager.java
+++
b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxAnnotationManager.java
@@ -27,6 +27,7 @@ import jakarta.inject.Inject;
import org.apache.james.mailbox.MailboxAnnotationManager;
import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.ReadOnlyAnnotationPredicate;
import org.apache.james.mailbox.exception.AnnotationException;
import org.apache.james.mailbox.exception.InsufficientRightsException;
import org.apache.james.mailbox.exception.MailboxException;
@@ -49,24 +50,41 @@ public class StoreMailboxAnnotationManager implements
MailboxAnnotationManager {
private final StoreRightManager rightManager;
private final int limitOfAnnotations;
private final int limitAnnotationSize;
+ private final ReadOnlyAnnotationPredicate readOnlyPredicate;
@Inject
public StoreMailboxAnnotationManager(MailboxSessionMapperFactory
mailboxSessionMapperFactory,
- StoreRightManager rightManager) {
+ StoreRightManager rightManager,
+ ReadOnlyAnnotationPredicate
readOnlyPredicate) {
this(mailboxSessionMapperFactory,
rightManager,
MailboxConstants.DEFAULT_LIMIT_ANNOTATIONS_ON_MAILBOX,
- MailboxConstants.DEFAULT_LIMIT_ANNOTATION_SIZE);
+ MailboxConstants.DEFAULT_LIMIT_ANNOTATION_SIZE,
+ readOnlyPredicate);
+ }
+
+ public StoreMailboxAnnotationManager(MailboxSessionMapperFactory
mailboxSessionMapperFactory, StoreRightManager rightManager) {
+ this(mailboxSessionMapperFactory, rightManager,
ReadOnlyAnnotationPredicate.ALLOW_ALL);
}
public StoreMailboxAnnotationManager(MailboxSessionMapperFactory
mailboxSessionMapperFactory,
StoreRightManager rightManager,
int limitOfAnnotations,
int limitAnnotationSize) {
+ this(mailboxSessionMapperFactory, rightManager, limitOfAnnotations,
limitAnnotationSize,
+ ReadOnlyAnnotationPredicate.ALLOW_ALL);
+ }
+
+ public StoreMailboxAnnotationManager(MailboxSessionMapperFactory
mailboxSessionMapperFactory,
+ StoreRightManager rightManager,
+ int limitOfAnnotations,
+ int limitAnnotationSize,
+ ReadOnlyAnnotationPredicate
readOnlyPredicate) {
this.mailboxSessionMapperFactory = mailboxSessionMapperFactory;
this.rightManager = rightManager;
this.limitOfAnnotations = limitOfAnnotations;
this.limitAnnotationSize = limitAnnotationSize;
+ this.readOnlyPredicate = readOnlyPredicate;
}
@Override
@@ -114,6 +132,9 @@ public class StoreMailboxAnnotationManager implements
MailboxAnnotationManager {
.switchIfEmpty(Mono.error(new
MailboxNotFoundException(mailboxPath)))
.flatMapMany(mailboxId -> Flux.fromIterable(mailboxAnnotations)
.concatMap(annotation -> {
+ if (readOnlyPredicate.isReadOnly(annotation.getKey())) {
+ return Mono.error(new AnnotationException("annotation
is read-only: " + annotation.getKey().asString()));
+ }
if (annotation.isNil()) {
return
Mono.from(annotationMapper.deleteAnnotationReactive(mailboxId,
annotation.getKey()));
}
diff --git
a/mailbox/store/src/test/java/org/apache/james/mailbox/store/StoreMailboxManagerAnnotationTest.java
b/mailbox/store/src/test/java/org/apache/james/mailbox/store/StoreMailboxManagerAnnotationTest.java
index 338f7a5299..9677c17d85 100644
---
a/mailbox/store/src/test/java/org/apache/james/mailbox/store/StoreMailboxManagerAnnotationTest.java
+++
b/mailbox/store/src/test/java/org/apache/james/mailbox/store/StoreMailboxManagerAnnotationTest.java
@@ -34,6 +34,8 @@ import java.util.Set;
import org.apache.james.core.Username;
import org.apache.james.mailbox.MailboxSession;
import org.apache.james.mailbox.MailboxSessionUtil;
+import org.apache.james.mailbox.ReadOnlyAnnotationPredicate;
+import org.apache.james.mailbox.exception.AnnotationException;
import org.apache.james.mailbox.exception.MailboxException;
import org.apache.james.mailbox.model.Mailbox;
import org.apache.james.mailbox.model.MailboxACL;
@@ -58,6 +60,7 @@ import reactor.core.publisher.Mono;
class StoreMailboxManagerAnnotationTest {
static final MailboxAnnotationKey PRIVATE_KEY = new
MailboxAnnotationKey("/private/comment");
static final MailboxAnnotationKey SHARED_KEY = new
MailboxAnnotationKey("/shared/comment");
+ static final MailboxAnnotationKey SERVER_KEY = new
MailboxAnnotationKey("/shared/vendor/server/quota");
static final MailboxAnnotation PRIVATE_ANNOTATION =
MailboxAnnotation.newInstance(PRIVATE_KEY, "My comment");
static final MailboxAnnotation SHARED_ANNOTATION =
MailboxAnnotation.newInstance(SHARED_KEY, "My shared comment");
@@ -102,7 +105,11 @@ class StoreMailboxManagerAnnotationTest {
.thenReturn(Mono.just(true));
annotationManager = spy(new
StoreMailboxAnnotationManager(mailboxSessionMapperFactory,
- storeRightManager));
+ storeRightManager,
AggregatedReadOnlyAnnotationPredicate.ALLOW_ALL));
+ }
+
+ private StoreMailboxAnnotationManager
managerWithReadOnly(ReadOnlyAnnotationPredicate predicate) {
+ return new StoreMailboxAnnotationManager(mailboxSessionMapperFactory,
storeRightManager, predicate);
}
@Test
@@ -172,4 +179,34 @@ class StoreMailboxManagerAnnotationTest {
assertThat(annotationManager.getAnnotationsByKeys(mailboxPath,
session, KEYS)).isEqualTo(ANNOTATIONS);
}
+
+ @Test
+ void updateAnnotationsShouldRejectReadOnlyKey() {
+ StoreMailboxAnnotationManager manager = managerWithReadOnly(key ->
key.equals(SERVER_KEY));
+ MailboxAnnotation serverAnnotation =
MailboxAnnotation.newInstance(SERVER_KEY, "42");
+
+ assertThatThrownBy(() -> manager.updateAnnotations(mailboxPath,
session, ImmutableList.of(serverAnnotation)))
+ .isInstanceOf(AnnotationException.class)
+ .hasMessageContaining("read-only");
+ }
+
+ @Test
+ void updateAnnotationsShouldRejectReadOnlyKeyEvenWhenNil() {
+ StoreMailboxAnnotationManager manager = managerWithReadOnly(key ->
key.equals(SERVER_KEY));
+
+ assertThatThrownBy(() -> manager.updateAnnotations(mailboxPath,
session, ImmutableList.of(MailboxAnnotation.nil(SERVER_KEY))))
+ .isInstanceOf(AnnotationException.class)
+ .hasMessageContaining("read-only");
+ }
+
+ @Test
+ void
updateAnnotationsShouldAllowWritableKeysWhenReadOnlyPredicateIsRegistered()
throws MailboxException {
+ StoreMailboxAnnotationManager manager = managerWithReadOnly(key ->
key.equals(SERVER_KEY));
+ when(annotationMapper.existReactive(eq(mailboxId),
any())).thenReturn(Mono.just(true));
+ when(annotationMapper.insertAnnotationReactive(eq(mailboxId),
any())).thenReturn(Mono.empty());
+
+ manager.updateAnnotations(mailboxPath, session,
ImmutableList.of(PRIVATE_ANNOTATION));
+
+ verify(annotationMapper,
times(1)).insertAnnotationReactive(eq(mailboxId), eq(PRIVATE_ANNOTATION));
+ }
}
diff --git
a/protocols/imap/src/test/java/org/apache/james/imap/processor/SetMetadataProcessorTest.java
b/protocols/imap/src/test/java/org/apache/james/imap/processor/SetMetadataProcessorTest.java
index 4a7c580a1a..267cb9279d 100644
---
a/protocols/imap/src/test/java/org/apache/james/imap/processor/SetMetadataProcessorTest.java
+++
b/protocols/imap/src/test/java/org/apache/james/imap/processor/SetMetadataProcessorTest.java
@@ -163,6 +163,20 @@ class SetMetadataProcessorTest {
verify(mockStatusResponseFactory, times(1)).taggedNo(any(Tag.class),
any(ImapCommand.class), humanTextCaptor.capture());
assertThat(humanTextCaptor.getAllValues().get(FIRST_ELEMENT_INDEX).getKey()).isEqualTo(HumanReadableText.MAILBOX_ANNOTATION_KEY);
+ }
+
+ @Test
+ void
processShouldResponseNoWithReadOnlyMessageWhenManagerThrowsReadOnlyAnnotationException()
{
+ MailboxAnnotationKey readOnlyKey = new
MailboxAnnotationKey("/private/key");
+ when(mockMailboxManager.updateAnnotationsReactive(eq(inbox),
eq(mockMailboxSession), eq(mailboxAnnotations)))
+ .thenReturn(Mono.error(new AnnotationException("annotation is
read-only: " + readOnlyKey.asString())));
+
+ processor.process(request, mockResponder, imapSession);
+
+ verify(mockStatusResponseFactory, times(1)).taggedNo(any(Tag.class),
any(ImapCommand.class), humanTextCaptor.capture());
+ HumanReadableText humanReadableText =
humanTextCaptor.getAllValues().get(FIRST_ELEMENT_INDEX);
+
assertThat(humanReadableText.getKey()).isEqualTo(HumanReadableText.MAILBOX_ANNOTATION_KEY);
+
assertThat(humanReadableText.getDefaultValue()).contains("read-only").contains(readOnlyKey.asString());
}
}
\ No newline at end of file
diff --git
a/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxModule.java
b/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxModule.java
index 0b6fefe568..00711819cd 100644
---
a/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxModule.java
+++
b/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxModule.java
@@ -22,6 +22,7 @@ import org.apache.james.mailbox.SystemMailboxesProvider;
import org.apache.james.mailbox.acl.MailboxACLResolver;
import org.apache.james.mailbox.acl.UnionMailboxACLResolver;
import org.apache.james.mailbox.store.SystemMailboxesProviderImpl;
+import org.apache.james.modules.mailbox.AnnotationModule;
import org.apache.james.modules.mailbox.MailReceptionHealthCheckModule;
import org.apache.james.modules.mailbox.PreDeletionHookModule;
import org.apache.james.utils.GuiceProbe;
@@ -36,6 +37,7 @@ public class MailboxModule extends AbstractModule {
protected void configure() {
install(new PreDeletionHookModule());
install(new MailReceptionHealthCheckModule());
+ install(new AnnotationModule());
Multibinder<GuiceProbe> probeMultiBinder =
Multibinder.newSetBinder(binder(), GuiceProbe.class);
probeMultiBinder.addBinding().to(MailboxProbeImpl.class);
diff --git
a/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxModule.java
b/server/container/guice/mailbox/src/main/java/org/apache/james/modules/mailbox/AnnotationModule.java
similarity index 52%
copy from
server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxModule.java
copy to
server/container/guice/mailbox/src/main/java/org/apache/james/modules/mailbox/AnnotationModule.java
index 0b6fefe568..dd5db27367 100644
---
a/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxModule.java
+++
b/server/container/guice/mailbox/src/main/java/org/apache/james/modules/mailbox/AnnotationModule.java
@@ -16,38 +16,33 @@
* specific language governing permissions and limitations *
* under the License. *
****************************************************************/
-package org.apache.james.modules;
-import org.apache.james.mailbox.SystemMailboxesProvider;
-import org.apache.james.mailbox.acl.MailboxACLResolver;
-import org.apache.james.mailbox.acl.UnionMailboxACLResolver;
-import org.apache.james.mailbox.store.SystemMailboxesProviderImpl;
-import org.apache.james.modules.mailbox.MailReceptionHealthCheckModule;
-import org.apache.james.modules.mailbox.PreDeletionHookModule;
-import org.apache.james.utils.GuiceProbe;
+package org.apache.james.modules.mailbox;
+
+import org.apache.james.mailbox.ReadOnlyAnnotationPredicate;
+import org.apache.james.mailbox.store.AggregatedReadOnlyAnnotationPredicate;
import com.google.inject.AbstractModule;
import com.google.inject.Scopes;
import com.google.inject.multibindings.Multibinder;
-public class MailboxModule extends AbstractModule {
+/**
+ * Wires the read-only annotation extension point.
+ *
+ * Other modules can contribute read-only keys by adding bindings to the
+ * {@code Set<ReadOnlyAnnotationPredicate>} multibinder, for example:
+ *
+ * <pre>
+ * Multibinder.newSetBinder(binder(), ReadOnlyAnnotationPredicate.class)
+ * .addBinding().toInstance(key ->
key.asString().startsWith("/shared/vendor/mymodule/"));
+ * </pre>
+ */
+public class AnnotationModule extends AbstractModule {
@Override
protected void configure() {
- install(new PreDeletionHookModule());
- install(new MailReceptionHealthCheckModule());
-
- Multibinder<GuiceProbe> probeMultiBinder =
Multibinder.newSetBinder(binder(), GuiceProbe.class);
- probeMultiBinder.addBinding().to(MailboxProbeImpl.class);
- probeMultiBinder.addBinding().to(QuotaProbesImpl.class);
- probeMultiBinder.addBinding().to(ACLProbeImpl.class);
- probeMultiBinder.addBinding().to(ConfigurationProbe.class);
-
- bind(UnionMailboxACLResolver.class).in(Scopes.SINGLETON);
- bind(MailboxACLResolver.class).to(UnionMailboxACLResolver.class);
-
- bind(SystemMailboxesProviderImpl.class).in(Scopes.SINGLETON);
-
bind(SystemMailboxesProvider.class).to(SystemMailboxesProviderImpl.class);
+ Multibinder.newSetBinder(binder(), ReadOnlyAnnotationPredicate.class);
+ bind(AggregatedReadOnlyAnnotationPredicate.class).in(Scopes.SINGLETON);
+
bind(ReadOnlyAnnotationPredicate.class).to(AggregatedReadOnlyAnnotationPredicate.class);
}
-
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]