You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by rc...@apache.org on 2024/04/23 03:17:47 UTC
(james-project) branch droplist updated: JAMES-3946 add a Cassandra implementation of the DropList (#2210)
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch droplist
in repository https://gitbox.apache.org/repos/asf/james-project.git
The following commit(s) were added to refs/heads/droplist by this push:
new 6873e67999 JAMES-3946 add a Cassandra implementation of the DropList (#2210)
6873e67999 is described below
commit 6873e679992e7845691cf625136d2fd8a2782a31
Author: Maksim <85...@users.noreply.github.com>
AuthorDate: Tue Apr 23 06:17:42 2024 +0300
JAMES-3946 add a Cassandra implementation of the DropList (#2210)
---
.../james/droplists/api/DropListContract.java | 43 +++---
.../droplists/cassandra/CassandraDropList.java | 68 ++++++++++
.../droplists/cassandra/CassandraDropListDAO.java | 147 +++++++++++++++++++++
.../cassandra/CassandraDropListModule.java | 35 +++++
.../cassandra/tables/CassandraDropListTable.java | 30 +++++
.../droplists/cassandra/CassandraDropListTest.java | 43 ++++++
.../james/droplists/memory/MemoryDropList.java | 8 +-
7 files changed, 342 insertions(+), 32 deletions(-)
diff --git a/server/data/data-api/src/test/java/org/apache/james/droplists/api/DropListContract.java b/server/data/data-api/src/test/java/org/apache/james/droplists/api/DropListContract.java
index a4993ff973..bdb21c79e1 100644
--- a/server/data/data-api/src/test/java/org/apache/james/droplists/api/DropListContract.java
+++ b/server/data/data-api/src/test/java/org/apache/james/droplists/api/DropListContract.java
@@ -40,32 +40,21 @@ public interface DropListContract {
DropList dropList();
- @Test
- default void shouldAddEntry() throws AddressException {
- DropListEntry dropListEntry = DropListEntry.builder()
- .forAll()
- .denyAddress(new MailAddress("denied@denied.com"))
- .build();
-
- Mono<Void> result = dropList().add(dropListEntry);
+ @ParameterizedTest(name = "{index} {0}")
+ @MethodSource("provideParametersForGetEntryListTest")
+ default void shouldAddEntry(DropListEntry dropListEntry) throws AddressException {
+ dropList().add(dropListEntry).block();
- assertThat(dropList().list(GLOBAL, dropListEntry.getOwner()).collectList().block().size()).isEqualTo(1);
- assertThat(result).isEqualTo(Mono.empty());
+ assertThat(dropList().list(dropListEntry.getOwnerScope(), dropListEntry.getOwner()).collectList().block().size()).isEqualTo(1);
}
- @Test
- default void shouldRemoveEntry() throws AddressException {
- DropListEntry dropListEntry = DropListEntry.builder()
- .forAll()
- .denyAddress(new MailAddress("denied@denied.com"))
- .build();
-
- dropList().add(dropListEntry);
-
- Mono<Void> result = dropList().remove(dropListEntry);
+ @ParameterizedTest(name = "{index} {0}")
+ @MethodSource("provideParametersForGetEntryListTest")
+ default void shouldRemoveEntry(DropListEntry dropListEntry) throws AddressException {
+ dropList().add(dropListEntry).block();
+ dropList().remove(dropListEntry).block();
- assertThat(dropList().list(GLOBAL, dropListEntry.getOwner()).collectList().block().size()).isZero();
- assertThat(result).isEqualTo(Mono.empty());
+ assertThat(dropList().list(dropListEntry.getOwnerScope(), dropListEntry.getOwner()).collectList().block().size()).isZero();
}
@Test
@@ -113,7 +102,7 @@ public interface DropListContract {
@ParameterizedTest(name = "{index} {0}")
@MethodSource("provideParametersForGetEntryListTest")
default void shouldGetEntryListForSpecifiedScopeAndOwner(DropListEntry dropListEntry) {
- dropList().add(dropListEntry);
+ dropList().add(dropListEntry).block();
Flux<DropListEntry> result = dropList().list(dropListEntry.getOwnerScope(), dropListEntry.getOwner());
@@ -121,20 +110,20 @@ public interface DropListContract {
}
- @ParameterizedTest(name = "{index} {0}")
+ @ParameterizedTest(name = "{index} {0}, sender: {1}")
@MethodSource("provideParametersForReturnAllowedTest")
default void shouldReturnAllowed(DropListEntry dropListEntry, MailAddress senderMailAddress) {
- dropList().add(dropListEntry);
+ dropList().add(dropListEntry).block();
Mono<DropList.Status> result = dropList().query(dropListEntry.getOwnerScope(), dropListEntry.getOwner(), senderMailAddress);
assertThat(result.block()).isEqualTo(DropList.Status.ALLOWED);
}
- @ParameterizedTest(name = "{index} {0}")
+ @ParameterizedTest(name = "{index} {0}, sender: {1}")
@MethodSource("provideParametersForReturnBlockedTest")
default void shouldReturnBlocked(DropListEntry dropListEntry, MailAddress senderMailAddress) {
- dropList().add(dropListEntry);
+ dropList().add(dropListEntry).block();
Mono<DropList.Status> result = dropList().query(dropListEntry.getOwnerScope(), dropListEntry.getOwner(), senderMailAddress);
diff --git a/server/data/data-cassandra/src/main/java/org/apache/james/droplists/cassandra/CassandraDropList.java b/server/data/data-cassandra/src/main/java/org/apache/james/droplists/cassandra/CassandraDropList.java
new file mode 100644
index 0000000000..98c5fd074a
--- /dev/null
+++ b/server/data/data-cassandra/src/main/java/org/apache/james/droplists/cassandra/CassandraDropList.java
@@ -0,0 +1,68 @@
+/****************************************************************
+ * 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.droplists.cassandra;
+
+import jakarta.inject.Inject;
+
+import org.apache.james.core.MailAddress;
+import org.apache.james.droplists.api.DropList;
+import org.apache.james.droplists.api.DropListEntry;
+import org.apache.james.droplists.api.OwnerScope;
+
+import com.google.common.base.Preconditions;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public class CassandraDropList implements DropList {
+
+ private final CassandraDropListDAO dropListDAO;
+
+ @Inject
+ public CassandraDropList(CassandraDropListDAO dropListDAO) {
+ this.dropListDAO = dropListDAO;
+ }
+
+ @Override
+ public Mono<Void> add(DropListEntry entry) {
+ Preconditions.checkArgument(entry != null);
+ return dropListDAO.addDropList(entry);
+ }
+
+ @Override
+ public Mono<Void> remove(DropListEntry entry) {
+ Preconditions.checkArgument(entry != null);
+ return dropListDAO.removeDropList(entry);
+ }
+
+ @Override
+ public Flux<DropListEntry> list(OwnerScope ownerScope, String owner) {
+ Preconditions.checkArgument(ownerScope != null);
+ Preconditions.checkArgument(owner != null);
+ return dropListDAO.getDropList(ownerScope, owner);
+ }
+
+ @Override
+ public Mono<Status> query(OwnerScope ownerScope, String owner, MailAddress sender) {
+ Preconditions.checkArgument(ownerScope != null);
+ Preconditions.checkArgument(owner != null);
+ Preconditions.checkArgument(sender != null);
+ return dropListDAO.queryDropList(ownerScope, owner, sender);
+ }
+}
\ No newline at end of file
diff --git a/server/data/data-cassandra/src/main/java/org/apache/james/droplists/cassandra/CassandraDropListDAO.java b/server/data/data-cassandra/src/main/java/org/apache/james/droplists/cassandra/CassandraDropListDAO.java
new file mode 100644
index 0000000000..be32a6f7bf
--- /dev/null
+++ b/server/data/data-cassandra/src/main/java/org/apache/james/droplists/cassandra/CassandraDropListDAO.java
@@ -0,0 +1,147 @@
+/****************************************************************
+ * 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.droplists.cassandra;
+
+import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker;
+import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.deleteFrom;
+import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.insertInto;
+import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.selectFrom;
+import static com.datastax.oss.driver.api.querybuilder.relation.Relation.column;
+import static org.apache.james.droplists.api.DeniedEntityType.DOMAIN;
+import static org.apache.james.droplists.cassandra.tables.CassandraDropListTable.DENIED_ENTITY;
+import static org.apache.james.droplists.cassandra.tables.CassandraDropListTable.DENIED_ENTITY_TYPE;
+import static org.apache.james.droplists.cassandra.tables.CassandraDropListTable.OWNER;
+import static org.apache.james.droplists.cassandra.tables.CassandraDropListTable.OWNER_SCOPE;
+import static org.apache.james.droplists.cassandra.tables.CassandraDropListTable.TABLE_NAME;
+
+import java.util.List;
+
+import jakarta.inject.Inject;
+import jakarta.mail.internet.AddressException;
+
+import org.apache.james.backends.cassandra.utils.CassandraAsyncExecutor;
+import org.apache.james.core.Domain;
+import org.apache.james.core.MailAddress;
+import org.apache.james.droplists.api.DropList;
+import org.apache.james.droplists.api.DropListEntry;
+import org.apache.james.droplists.api.OwnerScope;
+
+import com.datastax.oss.driver.api.core.CqlSession;
+import com.datastax.oss.driver.api.core.cql.PreparedStatement;
+import com.datastax.oss.driver.api.core.cql.Row;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public class CassandraDropListDAO {
+
+ private final CassandraAsyncExecutor executor;
+ private final PreparedStatement addDropListStatement;
+ private final PreparedStatement removeDropListStatement;
+ private final PreparedStatement getDropListStatement;
+ private final PreparedStatement queryDropListStatement;
+
+ @Inject
+ public CassandraDropListDAO(CqlSession session) {
+ this.executor = new CassandraAsyncExecutor(session);
+
+ addDropListStatement = session.prepare(insertInto(TABLE_NAME)
+ .value(OWNER_SCOPE, bindMarker(OWNER_SCOPE))
+ .value(OWNER, bindMarker(OWNER))
+ .value(DENIED_ENTITY, bindMarker(DENIED_ENTITY))
+ .value(DENIED_ENTITY_TYPE, bindMarker(DENIED_ENTITY_TYPE))
+ .ifNotExists()
+ .build());
+
+ removeDropListStatement = session.prepare(deleteFrom(TABLE_NAME)
+ .where(column(OWNER_SCOPE).isEqualTo(bindMarker(OWNER_SCOPE)),
+ column(OWNER).isEqualTo(bindMarker(OWNER)),
+ column(DENIED_ENTITY).isEqualTo(bindMarker(DENIED_ENTITY)))
+ .ifExists()
+ .build());
+
+ getDropListStatement = session.prepare(selectFrom(TABLE_NAME)
+ .all()
+ .where(column(OWNER_SCOPE).isEqualTo(bindMarker(OWNER_SCOPE)),
+ column(OWNER).isEqualTo(bindMarker(OWNER)))
+ .allowFiltering()
+ .build());
+
+ queryDropListStatement = session.prepare(selectFrom(TABLE_NAME)
+ .all()
+ .where(column(OWNER_SCOPE).isEqualTo(bindMarker(OWNER_SCOPE)),
+ column(OWNER).isEqualTo(bindMarker(OWNER)),
+ column(DENIED_ENTITY).in(bindMarker(DENIED_ENTITY)))
+ .build());
+ }
+
+ public Mono<Void> addDropList(DropListEntry dropListEntry) {
+ return executor.executeVoid(
+ addDropListStatement.bind()
+ .setString(OWNER_SCOPE, dropListEntry.getOwnerScope().name())
+ .setString(OWNER, dropListEntry.getOwner())
+ .setString(DENIED_ENTITY, dropListEntry.getDeniedEntity())
+ .setString(DENIED_ENTITY_TYPE, dropListEntry.getDeniedEntityType().name()));
+ }
+
+ public Mono<Void> removeDropList(DropListEntry dropListEntry) {
+ return executor.executeVoid(
+ removeDropListStatement.bind()
+ .setString(OWNER_SCOPE, dropListEntry.getOwnerScope().name())
+ .setString(OWNER, dropListEntry.getOwner())
+ .setString(DENIED_ENTITY, dropListEntry.getDeniedEntity()));
+ }
+
+ public Mono<DropList.Status> queryDropList(OwnerScope ownerScope, String owner, MailAddress sender) {
+ return executor.executeReturnExists(
+ queryDropListStatement.bind()
+ .setString(OWNER_SCOPE, ownerScope.name())
+ .setString(OWNER, owner)
+ .setList(DENIED_ENTITY, List.of(sender.asString(), sender.getDomain().asString()), String.class))
+ .map(isExist -> Boolean.TRUE.equals(isExist) ? DropList.Status.BLOCKED : DropList.Status.ALLOWED);
+ }
+
+ public Flux<DropListEntry> getDropList(OwnerScope ownerScope, String owner) {
+ return executor.executeRows(getDropListStatement.bind()
+ .setString(OWNER_SCOPE, ownerScope.name())
+ .setString(OWNER, owner))
+ .map(row -> mapRowToDropListEntry(ownerScope, row));
+ }
+
+ private static DropListEntry mapRowToDropListEntry(OwnerScope ownerScope, Row row) {
+ String deniedEntity = row.getString(DENIED_ENTITY);
+ String deniedEntityType = row.getString(DENIED_ENTITY_TYPE);
+ try {
+ DropListEntry.Builder builder = DropListEntry.builder();
+ switch (ownerScope) {
+ case USER -> builder.userOwner(new MailAddress(row.getString(OWNER)));
+ case DOMAIN -> builder.domainOwner(Domain.of(row.getString(OWNER)));
+ case GLOBAL -> builder.forAll();
+ }
+ if (DOMAIN.name().equals(deniedEntityType)) {
+ builder.denyDomain(Domain.of(deniedEntity));
+ } else {
+ builder.denyAddress(new MailAddress(deniedEntity));
+ }
+ return builder.build();
+ } catch (AddressException e) {
+ throw new IllegalArgumentException("Entity could not be parsed as a MailAddress", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/server/data/data-cassandra/src/main/java/org/apache/james/droplists/cassandra/CassandraDropListModule.java b/server/data/data-cassandra/src/main/java/org/apache/james/droplists/cassandra/CassandraDropListModule.java
new file mode 100644
index 0000000000..b555c1c2a0
--- /dev/null
+++ b/server/data/data-cassandra/src/main/java/org/apache/james/droplists/cassandra/CassandraDropListModule.java
@@ -0,0 +1,35 @@
+/****************************************************************
+ * 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.droplists.cassandra;
+
+import org.apache.james.backends.cassandra.components.CassandraModule;
+import org.apache.james.droplists.cassandra.tables.CassandraDropListTable;
+
+import com.datastax.oss.driver.api.core.type.DataTypes;
+
+public interface CassandraDropListModule {
+ CassandraModule MODULE = CassandraModule.table(CassandraDropListTable.TABLE_NAME)
+ .comment("Holds DropLists of this James server.")
+ .statement(statement -> types -> statement
+ .withPartitionKey(CassandraDropListTable.OWNER_SCOPE, DataTypes.TEXT)
+ .withPartitionKey(CassandraDropListTable.OWNER, DataTypes.TEXT)
+ .withPartitionKey(CassandraDropListTable.DENIED_ENTITY, DataTypes.TEXT)
+ .withColumn(CassandraDropListTable.DENIED_ENTITY_TYPE, DataTypes.TEXT))
+ .build();
+}
\ No newline at end of file
diff --git a/server/data/data-cassandra/src/main/java/org/apache/james/droplists/cassandra/tables/CassandraDropListTable.java b/server/data/data-cassandra/src/main/java/org/apache/james/droplists/cassandra/tables/CassandraDropListTable.java
new file mode 100644
index 0000000000..5a42a98fec
--- /dev/null
+++ b/server/data/data-cassandra/src/main/java/org/apache/james/droplists/cassandra/tables/CassandraDropListTable.java
@@ -0,0 +1,30 @@
+/****************************************************************
+ * 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.droplists.cassandra.tables;
+
+import com.datastax.oss.driver.api.core.CqlIdentifier;
+
+public interface CassandraDropListTable {
+
+ String TABLE_NAME = "droplist";
+ CqlIdentifier OWNER_SCOPE = CqlIdentifier.fromCql("owner_scope");
+ CqlIdentifier OWNER = CqlIdentifier.fromCql("owner");
+ CqlIdentifier DENIED_ENTITY_TYPE = CqlIdentifier.fromCql("denied_entity_type");
+ CqlIdentifier DENIED_ENTITY = CqlIdentifier.fromCql("denied_entity");
+}
\ No newline at end of file
diff --git a/server/data/data-cassandra/src/test/java/org/apache/james/droplists/cassandra/CassandraDropListTest.java b/server/data/data-cassandra/src/test/java/org/apache/james/droplists/cassandra/CassandraDropListTest.java
new file mode 100644
index 0000000000..0871d00df7
--- /dev/null
+++ b/server/data/data-cassandra/src/test/java/org/apache/james/droplists/cassandra/CassandraDropListTest.java
@@ -0,0 +1,43 @@
+/****************************************************************
+ * 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.droplists.cassandra;
+
+import org.apache.james.backends.cassandra.CassandraCluster;
+import org.apache.james.backends.cassandra.CassandraClusterExtension;
+import org.apache.james.droplists.api.DropList;
+import org.apache.james.droplists.api.DropListContract;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class CassandraDropListTest implements DropListContract {
+ @RegisterExtension
+ static CassandraClusterExtension cassandraCluster = new CassandraClusterExtension(CassandraDropListModule.MODULE);
+
+ DropList dropList;
+
+ @BeforeEach
+ public void setUp(CassandraCluster cassandra) throws Exception {
+ dropList = new CassandraDropList(new CassandraDropListDAO(cassandra.getConf()));
+ }
+
+ @Override
+ public DropList dropList() {
+ return dropList;
+ }
+}
\ No newline at end of file
diff --git a/server/data/data-memory/src/main/java/org/apache/james/droplists/memory/MemoryDropList.java b/server/data/data-memory/src/main/java/org/apache/james/droplists/memory/MemoryDropList.java
index b413d4f919..db803fe358 100644
--- a/server/data/data-memory/src/main/java/org/apache/james/droplists/memory/MemoryDropList.java
+++ b/server/data/data-memory/src/main/java/org/apache/james/droplists/memory/MemoryDropList.java
@@ -43,8 +43,7 @@ public class MemoryDropList implements DropList {
Preconditions.checkArgument(entry != null);
OwnerScope ownerScope = entry.getOwnerScope();
Multimap<OwnerScope, DropListEntry> selectedDropList = getDropListByScope(ownerScope);
- selectedDropList.put(ownerScope, entry);
- return Mono.empty();
+ return Mono.fromRunnable(() -> selectedDropList.put(ownerScope, entry));
}
@Override
@@ -52,8 +51,7 @@ public class MemoryDropList implements DropList {
Preconditions.checkArgument(entry != null);
OwnerScope ownerScope = entry.getOwnerScope();
Multimap<OwnerScope, DropListEntry> selectedDropList = getDropListByScope(ownerScope);
- selectedDropList.remove(ownerScope, entry);
- return Mono.empty();
+ return Mono.fromRunnable(() -> selectedDropList.remove(ownerScope, entry));
}
@Override
@@ -94,4 +92,4 @@ public class MemoryDropList implements DropList {
return entry.getDeniedEntity().equals(entityFromSender);
}
-}
+}
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org