You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2023/01/09 02:36:59 UTC

[james-project] branch master updated: JAMES-3756 Delegation store should allow listing accounts delegated to me (#1372)

This is an automated email from the ASF dual-hosted git repository.

btellier 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 7cd91b5158 JAMES-3756 Delegation store should allow listing accounts delegated to me (#1372)
7cd91b5158 is described below

commit 7cd91b5158536cc2dfc254376c7acd97263f3343
Author: vttran <vt...@linagora.com>
AuthorDate: Mon Jan 9 09:36:53 2023 +0700

    JAMES-3756 Delegation store should allow listing accounts delegated to me (#1372)
---
 .../org/apache/james/user/api/DelegationStore.java |   5 +
 .../james/user/api/DelegationStoreContract.java    | 119 ++++++++++++++++++++-
 .../user/cassandra/CassandraDelegationStore.java   |  10 ++
 .../james/user/cassandra/CassandraUsersDAO.java    |  92 ++++++++++++++--
 .../cassandra/CassandraUsersRepositoryModule.java  |   3 +-
 .../user/cassandra/tables/CassandraUserTable.java  |   1 +
 .../cassandra/CassandraDelegationStoreTest.java    |  14 ++-
 .../james/user/memory/MemoryDelegationStore.java   |  15 +++
 .../james/user/memory/NaiveDelegationStore.java    |  10 ++
 upgrade-instructions.md                            |  16 +++
 10 files changed, 274 insertions(+), 11 deletions(-)

diff --git a/server/data/data-api/src/main/java/org/apache/james/user/api/DelegationStore.java b/server/data/data-api/src/main/java/org/apache/james/user/api/DelegationStore.java
index b8cd22b5f8..25d86eae4c 100644
--- a/server/data/data-api/src/main/java/org/apache/james/user/api/DelegationStore.java
+++ b/server/data/data-api/src/main/java/org/apache/james/user/api/DelegationStore.java
@@ -45,4 +45,9 @@ public interface DelegationStore {
     default Fluent removeAuthorizedUser(Username userWithAccess) {
         return baseUser -> removeAuthorizedUser(baseUser, userWithAccess);
     }
+
+    Publisher<Username> delegatedUsers(Username baseUser);
+
+    Publisher<Void> removeDelegatedUser(Username baseUser, Username delegatedToUser);
+
 }
diff --git a/server/data/data-api/src/test/java/org/apache/james/user/api/DelegationStoreContract.java b/server/data/data-api/src/test/java/org/apache/james/user/api/DelegationStoreContract.java
index 2375a5e0fd..7a398ab49f 100644
--- a/server/data/data-api/src/test/java/org/apache/james/user/api/DelegationStoreContract.java
+++ b/server/data/data-api/src/test/java/org/apache/james/user/api/DelegationStoreContract.java
@@ -20,6 +20,7 @@
 package org.apache.james.user.api;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
 
 import org.apache.james.core.Username;
 import org.junit.jupiter.api.Test;
@@ -37,8 +38,11 @@ public interface DelegationStoreContract {
 
     DelegationStore testee();
 
+    default void addUser(Username username) {
+    }
+
     @Test
-    default void authorizedUsersShouldReturnEmptyByDefult() {
+    default void authorizedUsersShouldReturnEmptyByDefault() {
         assertThat(Flux.from(testee().authorizedUsers(ALICE)).collectList().block())
             .isEmpty();
     }
@@ -67,6 +71,8 @@ public interface DelegationStoreContract {
 
     @Test
     default void clearShouldBeIdempotent() {
+        addUser(BOB);
+        Mono.from(testee().addAuthorizedUser(EDGARD, BOB)).block();
         Mono.from(testee().addAuthorizedUser(ALICE, BOB)).block();
         Mono.from(testee().addAuthorizedUser(ALICE, CEDRIC)).block();
         Mono.from(testee().addAuthorizedUser(ALICE, DAMIEN)).block();
@@ -76,6 +82,8 @@ public interface DelegationStoreContract {
 
         assertThat(Flux.from(testee().authorizedUsers(ALICE)).collectList().block())
             .isEmpty();
+        assertThat(Flux.from(testee().delegatedUsers(BOB)).collectList().block())
+            .containsExactly(EDGARD);
     }
 
     @Test
@@ -92,15 +100,20 @@ public interface DelegationStoreContract {
 
     @Test
     default void removeAuthorizedUserShouldBeIdempotent() {
+        addUser(CEDRIC);
         Mono.from(testee().addAuthorizedUser(ALICE, BOB)).block();
         Mono.from(testee().addAuthorizedUser(ALICE, CEDRIC)).block();
         Mono.from(testee().addAuthorizedUser(ALICE, DAMIEN)).block();
+        Mono.from(testee().addAuthorizedUser(EDGARD, CEDRIC)).block();
 
         Mono.from(testee().removeAuthorizedUser(ALICE, CEDRIC)).block();
         Mono.from(testee().removeAuthorizedUser(ALICE, CEDRIC)).block();
 
         assertThat(Flux.from(testee().authorizedUsers(ALICE)).collectList().block())
             .containsOnly(BOB, DAMIEN);
+
+        assertThat(Flux.from(testee().delegatedUsers(CEDRIC)).collectList().block())
+            .containsOnly(EDGARD);
     }
 
     @Test
@@ -121,4 +134,108 @@ public interface DelegationStoreContract {
             .isEmpty();
     }
 
+    @Test
+    default void delegatedUsersShouldReturnEmptyByDefault() {
+        assertThat(Flux.from(testee().delegatedUsers(BOB)).collectList().block())
+            .isEmpty();
+    }
+
+    @Test
+    default void delegatedUsersShouldReturnCorrectUsers() {
+        addUser(BOB);
+        Mono.from(testee().addAuthorizedUser(ALICE, BOB)).block();
+        Mono.from(testee().addAuthorizedUser(CEDRIC, BOB)).block();
+
+        assertThat(Flux.from(testee().delegatedUsers(BOB)).collectList().block())
+            .containsOnly(CEDRIC, ALICE);
+    }
+
+    @Test
+    default void delegatedUsersShouldReturnUpdateEntryAfterClearDelegatedBaseUser() {
+        addUser(BOB);
+        Mono.from(testee().addAuthorizedUser(ALICE, BOB)).block();
+        Mono.from(testee().addAuthorizedUser(CEDRIC, BOB)).block();
+
+        Mono.from(testee().clear(ALICE)).block();
+        assertThat(Flux.from(testee().delegatedUsers(BOB)).collectList().block())
+            .containsOnly(CEDRIC);
+    }
+
+    @Test
+    default void delegatedUsersShouldNotReturnDeletedUsers() {
+        addUser(BOB);
+        Mono.from(testee().addAuthorizedUser(ALICE, BOB)).block();
+        Mono.from(testee().addAuthorizedUser(CEDRIC, BOB)).block();
+        Mono.from(testee().addAuthorizedUser(DAMIEN, BOB)).block();
+
+        Mono.from(testee().removeAuthorizedUser(ALICE, BOB)).block();
+
+        assertThat(Flux.from(testee().delegatedUsers(BOB)).collectList().block())
+            .containsOnly(CEDRIC, DAMIEN);
+    }
+
+    @Test
+    default void delegatedUsersShouldNotReturnDuplicates() {
+        addUser(BOB);
+        Mono.from(testee().addAuthorizedUser(ALICE, BOB)).block();
+        Mono.from(testee().addAuthorizedUser(ALICE, BOB)).block();
+
+        assertThat(Flux.from(testee().delegatedUsers(BOB)).collectList().block())
+            .containsExactly(ALICE);
+    }
+
+    @Test
+    default void delegatedUsersShouldNotReturnUnrelatedUsers() {
+        addUser(BOB);
+        Mono.from(testee().addAuthorizedUser(BOB, ALICE)).block();
+        Mono.from(testee().addAuthorizedUser(BOB, CEDRIC)).block();
+
+        assertThat(Flux.from(testee().delegatedUsers(BOB)).collectList().block())
+            .isEmpty();
+    }
+
+    @Test
+    default void addAuthorizedUserShouldNotThrowWhenUserWithAccessDoesNotExist() {
+        assertThatCode(() -> Mono.from(testee().addAuthorizedUser(BOB, ALICE)).block())
+            .doesNotThrowAnyException();
+    }
+
+
+    @Test
+    default void delegatedUserShouldNotReturnDeletedUsers() {
+        addUser(BOB);
+        Mono.from(testee().addAuthorizedUser(ALICE, BOB)).block();
+        Mono.from(testee().addAuthorizedUser(CEDRIC, BOB)).block();
+
+        Mono.from(testee().removeDelegatedUser(BOB, CEDRIC)).block();
+
+        assertThat(Flux.from(testee().delegatedUsers(BOB)).collectList().block())
+            .containsOnly(ALICE);
+    }
+
+    @Test
+    default void removeDelegatedUserShouldBeIdempotent() {
+        addUser(BOB);
+        Mono.from(testee().addAuthorizedUser(ALICE, BOB)).block();
+        Mono.from(testee().addAuthorizedUser(CEDRIC, BOB)).block();
+
+        Mono.from(testee().removeDelegatedUser(BOB, CEDRIC)).block();
+        Mono.from(testee().removeDelegatedUser(BOB, CEDRIC)).block();
+
+        assertThat(Flux.from(testee().delegatedUsers(BOB)).collectList().block())
+            .containsOnly(ALICE);
+    }
+
+    @Test
+    default void removeDelegatedUserShouldUpdateAuthorizedUserRelated() {
+        addUser(BOB);
+        Mono.from(testee().addAuthorizedUser(ALICE, BOB)).block();
+        Mono.from(testee().addAuthorizedUser(ALICE, CEDRIC)).block();
+
+        Mono.from(testee().removeDelegatedUser(BOB, ALICE)).block();
+
+        assertThat(Flux.from(testee().authorizedUsers(ALICE)).collectList().block())
+            .containsOnly(CEDRIC);
+    }
+
 }
diff --git a/server/data/data-cassandra/src/main/java/org/apache/james/user/cassandra/CassandraDelegationStore.java b/server/data/data-cassandra/src/main/java/org/apache/james/user/cassandra/CassandraDelegationStore.java
index c95bb10ece..9fc32c1bfa 100644
--- a/server/data/data-cassandra/src/main/java/org/apache/james/user/cassandra/CassandraDelegationStore.java
+++ b/server/data/data-cassandra/src/main/java/org/apache/james/user/cassandra/CassandraDelegationStore.java
@@ -52,4 +52,14 @@ public class CassandraDelegationStore implements DelegationStore {
     public Publisher<Void> removeAuthorizedUser(Username baseUser, Username userWithAccess) {
         return cassandraUsersDAO.removeAuthorizedUser(baseUser, userWithAccess);
     }
+
+    @Override
+    public Publisher<Username> delegatedUsers(Username baseUser) {
+        return cassandraUsersDAO.getDelegatedToUsers(baseUser);
+    }
+
+    @Override
+    public Publisher<Void> removeDelegatedUser(Username baseUser, Username delegatedToUser) {
+        return cassandraUsersDAO.removeDelegatedToUser(baseUser, delegatedToUser);
+    }
 }
diff --git a/server/data/data-cassandra/src/main/java/org/apache/james/user/cassandra/CassandraUsersDAO.java b/server/data/data-cassandra/src/main/java/org/apache/james/user/cassandra/CassandraUsersDAO.java
index a64bbad2d0..2732a87e08 100644
--- a/server/data/data-cassandra/src/main/java/org/apache/james/user/cassandra/CassandraUsersDAO.java
+++ b/server/data/data-cassandra/src/main/java/org/apache/james/user/cassandra/CassandraUsersDAO.java
@@ -27,6 +27,7 @@ import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.selectFrom;
 import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.update;
 import static org.apache.james.user.cassandra.tables.CassandraUserTable.ALGORITHM;
 import static org.apache.james.user.cassandra.tables.CassandraUserTable.AUTHORIZED_USERS;
+import static org.apache.james.user.cassandra.tables.CassandraUserTable.DELEGATED_USERS;
 import static org.apache.james.user.cassandra.tables.CassandraUserTable.NAME;
 import static org.apache.james.user.cassandra.tables.CassandraUserTable.PASSWORD;
 import static org.apache.james.user.cassandra.tables.CassandraUserTable.REALNAME;
@@ -46,10 +47,15 @@ import org.apache.james.user.lib.UsersDAO;
 import org.apache.james.user.lib.model.Algorithm;
 import org.apache.james.user.lib.model.Algorithm.HashingMode;
 import org.apache.james.user.lib.model.DefaultUser;
+import org.apache.james.util.FunctionalUtils;
 import org.reactivestreams.Publisher;
 
 import com.datastax.oss.driver.api.core.CqlSession;
+import com.datastax.oss.driver.api.core.cql.BatchStatementBuilder;
+import com.datastax.oss.driver.api.core.cql.BatchType;
+import com.datastax.oss.driver.api.core.cql.BoundStatement;
 import com.datastax.oss.driver.api.core.cql.PreparedStatement;
+import com.datastax.oss.driver.api.core.cql.Statement;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableSet;
@@ -72,6 +78,10 @@ public class CassandraUsersDAO implements UsersDAO {
     private final PreparedStatement addAuthorizedUsersStatement;
     private final PreparedStatement removeAuthorizedUsersStatement;
 
+    private final PreparedStatement getDelegatedToUsersStatement;
+    private final PreparedStatement addDelegatedToUsersStatement;
+    private final PreparedStatement removeDelegatedToUsersStatement;
+
     private final Algorithm preferredAlgorithm;
     private final HashingMode fallbackHashingMode;
 
@@ -134,6 +144,21 @@ public class CassandraUsersDAO implements UsersDAO {
             .remove(AUTHORIZED_USERS, bindMarker(AUTHORIZED_USERS))
             .whereColumn(NAME).isEqualTo(bindMarker(NAME))
             .build());
+
+        this.getDelegatedToUsersStatement = session.prepare(selectFrom(TABLE_NAME)
+            .columns(DELEGATED_USERS)
+            .whereColumn(NAME).isEqualTo(bindMarker(NAME))
+            .build());
+
+        this.addDelegatedToUsersStatement = session.prepare(update(TABLE_NAME)
+            .append(DELEGATED_USERS, bindMarker(DELEGATED_USERS))
+            .whereColumn(NAME).isEqualTo(bindMarker(NAME))
+            .build());
+
+        this.removeDelegatedToUsersStatement = session.prepare(update(TABLE_NAME)
+            .remove(DELEGATED_USERS, bindMarker(DELEGATED_USERS))
+            .whereColumn(NAME).isEqualTo(bindMarker(NAME))
+            .build());
     }
 
     @VisibleForTesting
@@ -155,6 +180,11 @@ public class CassandraUsersDAO implements UsersDAO {
                 Algorithm.of(row.getString(ALGORITHM), fallbackHashingMode), preferredAlgorithm));
     }
 
+    public Mono<Boolean> exist(Username name) {
+        return executor.executeReturnExists(getUserStatement.bind()
+            .setString(NAME, name.asString()));
+    }
+
     @Override
     public void updateUser(User user) throws UsersRepositoryException {
         Preconditions.checkArgument(user instanceof DefaultUser);
@@ -173,21 +203,47 @@ public class CassandraUsersDAO implements UsersDAO {
     }
 
     public Mono<Void> addAuthorizedUsers(Username baseUser, Username userWithAccess) {
-        return executor.executeVoid(addAuthorizedUsersStatement.bind()
+        BatchStatementBuilder batchBuilder = new BatchStatementBuilder(BatchType.LOGGED);
+        BoundStatement addAuthorizedStatement = addAuthorizedUsersStatement.bind()
             .setString(NAME, baseUser.asString())
-            .setSet(AUTHORIZED_USERS, ImmutableSet.of(userWithAccess.asString()), String.class));
+            .setSet(AUTHORIZED_USERS, ImmutableSet.of(userWithAccess.asString()), String.class);
+        batchBuilder.addStatement(addAuthorizedStatement);
+        batchBuilder.addStatement(addDelegatedToUsersStatement.bind()
+            .setString(NAME, userWithAccess.asString())
+            .setSet(DELEGATED_USERS, ImmutableSet.of(baseUser.asString()), String.class));
+
+        return getUserByNameReactive(userWithAccess).hasElement()
+            .filter(FunctionalUtils.identityPredicate())
+            .map(existAuthorizedUser -> (Statement) batchBuilder.build())
+            .switchIfEmpty(Mono.just(addAuthorizedStatement))
+            .flatMap(executor::executeVoid);
     }
 
     public Mono<Void> removeAuthorizedUser(Username baseUser, Username userWithAccess) {
-        return executor.executeVoid(removeAuthorizedUsersStatement.bind()
-            .setString(NAME, baseUser.asString())
-            .setSet(AUTHORIZED_USERS, ImmutableSet.of(userWithAccess.asString()), String.class));
+        return executor.executeVoid(new BatchStatementBuilder(BatchType.LOGGED)
+            .addStatement(removeAuthorizedUsersStatement.bind()
+                .setString(NAME, baseUser.asString())
+                .setSet(AUTHORIZED_USERS, ImmutableSet.of(userWithAccess.asString()), String.class))
+            .addStatement(removeDelegatedToUsersStatement.bind()
+                .setString(NAME, userWithAccess.asString())
+                .setSet(DELEGATED_USERS, ImmutableSet.of(baseUser.asString()), String.class))
+            .build());
     }
 
     public Mono<Void> removeAllAuthorizedUsers(Username baseUser) {
-        return executor.executeVoid(
-            removeAllAuthorizedUsersStatement.bind()
-                .setString(NAME, baseUser.asString()));
+        return getAuthorizedUsers(baseUser)
+            .collectList()
+            .map(authorizedList -> {
+                BatchStatementBuilder batch = new BatchStatementBuilder(BatchType.LOGGED);
+                authorizedList.forEach(username -> batch.addStatement(
+                    removeDelegatedToUsersStatement.bind()
+                        .setString(NAME, username.asString())
+                        .setSet(DELEGATED_USERS, ImmutableSet.of(baseUser.asString()), String.class)));
+                batch.addStatement(removeAllAuthorizedUsersStatement.bind()
+                    .setString(NAME, baseUser.asString()));
+                return batch.build();
+            })
+            .flatMap(executor::executeVoid);
     }
 
     public Flux<Username> getAuthorizedUsers(Username name) {
@@ -199,6 +255,26 @@ public class CassandraUsersDAO implements UsersDAO {
             .map(Username::of);
     }
 
+    public Mono<Void> removeDelegatedToUser(Username baseUser, Username delegatedToUser) {
+        return executor.executeVoid(new BatchStatementBuilder(BatchType.LOGGED)
+            .addStatement(removeAuthorizedUsersStatement.bind()
+                .setString(NAME, delegatedToUser.asString())
+                .setSet(AUTHORIZED_USERS, ImmutableSet.of(baseUser.asString()), String.class))
+            .addStatement(removeDelegatedToUsersStatement.bind()
+                .setString(NAME, baseUser.asString())
+                .setSet(DELEGATED_USERS, ImmutableSet.of(delegatedToUser.asString()), String.class))
+            .build());
+    }
+
+    public Flux<Username> getDelegatedToUsers(Username name) {
+        return executor.executeSingleRow(
+                getDelegatedToUsersStatement.bind()
+                    .setString(NAME, name.asString()))
+            .mapNotNull(row -> row.getSet(DELEGATED_USERS, String.class))
+            .flatMapIterable(set -> set)
+            .map(Username::of);
+    }
+
     @Override
     public void removeUser(Username name) throws UsersRepositoryException {
         boolean executed = executor.executeReturnApplied(
diff --git a/server/data/data-cassandra/src/main/java/org/apache/james/user/cassandra/CassandraUsersRepositoryModule.java b/server/data/data-cassandra/src/main/java/org/apache/james/user/cassandra/CassandraUsersRepositoryModule.java
index 6ec22033cf..16e34f96cb 100644
--- a/server/data/data-cassandra/src/main/java/org/apache/james/user/cassandra/CassandraUsersRepositoryModule.java
+++ b/server/data/data-cassandra/src/main/java/org/apache/james/user/cassandra/CassandraUsersRepositoryModule.java
@@ -32,6 +32,7 @@ public interface CassandraUsersRepositoryModule {
             .withColumn(CassandraUserTable.REALNAME, DataTypes.TEXT)
             .withColumn(CassandraUserTable.PASSWORD, DataTypes.TEXT)
             .withColumn(CassandraUserTable.ALGORITHM, DataTypes.TEXT)
-            .withColumn(CassandraUserTable.AUTHORIZED_USERS, DataTypes.setOf(DataTypes.TEXT)))
+            .withColumn(CassandraUserTable.AUTHORIZED_USERS, DataTypes.setOf(DataTypes.TEXT))
+            .withColumn(CassandraUserTable.DELEGATED_USERS, DataTypes.setOf(DataTypes.TEXT)))
         .build();
 }
diff --git a/server/data/data-cassandra/src/main/java/org/apache/james/user/cassandra/tables/CassandraUserTable.java b/server/data/data-cassandra/src/main/java/org/apache/james/user/cassandra/tables/CassandraUserTable.java
index fbac598ade..2e5c4d0fcd 100644
--- a/server/data/data-cassandra/src/main/java/org/apache/james/user/cassandra/tables/CassandraUserTable.java
+++ b/server/data/data-cassandra/src/main/java/org/apache/james/user/cassandra/tables/CassandraUserTable.java
@@ -29,4 +29,5 @@ public interface CassandraUserTable {
     CqlIdentifier PASSWORD = CqlIdentifier.fromCql("passwd");
     CqlIdentifier REALNAME = CqlIdentifier.fromCql("realname");
     CqlIdentifier AUTHORIZED_USERS = CqlIdentifier.fromCql("authorized_users");
+    CqlIdentifier DELEGATED_USERS = CqlIdentifier.fromCql("delegated_users");
 }
diff --git a/server/data/data-cassandra/src/test/java/org/apache/james/user/cassandra/CassandraDelegationStoreTest.java b/server/data/data-cassandra/src/test/java/org/apache/james/user/cassandra/CassandraDelegationStoreTest.java
index 22067d17f1..f190189e39 100644
--- a/server/data/data-cassandra/src/test/java/org/apache/james/user/cassandra/CassandraDelegationStoreTest.java
+++ b/server/data/data-cassandra/src/test/java/org/apache/james/user/cassandra/CassandraDelegationStoreTest.java
@@ -20,8 +20,10 @@
 package org.apache.james.user.cassandra;
 
 import org.apache.james.backends.cassandra.CassandraClusterExtension;
+import org.apache.james.core.Username;
 import org.apache.james.user.api.DelegationStore;
 import org.apache.james.user.api.DelegationStoreContract;
+import org.apache.james.user.api.UsersRepositoryException;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
@@ -30,10 +32,12 @@ public class CassandraDelegationStoreTest implements DelegationStoreContract {
     static CassandraClusterExtension cassandraCluster = new CassandraClusterExtension(CassandraUsersRepositoryModule.MODULE);
 
     private CassandraDelegationStore testee;
+    private CassandraUsersDAO cassandraUsersDAO;
 
     @BeforeEach
     void setUp() {
-        testee = new CassandraDelegationStore(new CassandraUsersDAO(cassandraCluster.getCassandraCluster().getConf()));
+        cassandraUsersDAO = new CassandraUsersDAO(cassandraCluster.getCassandraCluster().getConf());
+        testee = new CassandraDelegationStore(cassandraUsersDAO);
     }
 
     @Override
@@ -41,4 +45,12 @@ public class CassandraDelegationStoreTest implements DelegationStoreContract {
         return testee;
     }
 
+    @Override
+    public void addUser(Username username) {
+        try {
+            cassandraUsersDAO.addUser(username, "password");
+        } catch (UsersRepositoryException e) {
+            throw new RuntimeException(e);
+        }
+    }
 }
diff --git a/server/data/data-memory/src/main/java/org/apache/james/user/memory/MemoryDelegationStore.java b/server/data/data-memory/src/main/java/org/apache/james/user/memory/MemoryDelegationStore.java
index fed54584db..637ab7cdfd 100644
--- a/server/data/data-memory/src/main/java/org/apache/james/user/memory/MemoryDelegationStore.java
+++ b/server/data/data-memory/src/main/java/org/apache/james/user/memory/MemoryDelegationStore.java
@@ -18,6 +18,8 @@
  ****************************************************************/
 package org.apache.james.user.memory;
 
+import java.util.Map;
+
 import org.apache.james.core.Username;
 import org.apache.james.user.api.DelegationStore;
 import org.reactivestreams.Publisher;
@@ -52,6 +54,19 @@ public class MemoryDelegationStore implements DelegationStore {
         return Mono.fromRunnable(() -> delegations.remove(baseUser, userWithAccess));
     }
 
+    @Override
+    public Publisher<Username> delegatedUsers(Username baseUser) {
+        return Flux.fromIterable(delegations.entries())
+            .filter(entry -> entry.getValue().equals(baseUser))
+            .map(Map.Entry::getKey)
+            .distinct();
+    }
+
+    @Override
+    public Publisher<Void> removeDelegatedUser(Username baseUser, Username delegatedToUser) {
+        return Mono.fromRunnable(() -> delegations.remove(delegatedToUser, baseUser));
+    }
+
     @Override
     public Publisher<Void> clear(Username baseUser) {
         return Mono.fromRunnable(() -> delegations.removeAll(baseUser));
diff --git a/server/data/data-memory/src/main/java/org/apache/james/user/memory/NaiveDelegationStore.java b/server/data/data-memory/src/main/java/org/apache/james/user/memory/NaiveDelegationStore.java
index fbd6e4007c..90db00152c 100644
--- a/server/data/data-memory/src/main/java/org/apache/james/user/memory/NaiveDelegationStore.java
+++ b/server/data/data-memory/src/main/java/org/apache/james/user/memory/NaiveDelegationStore.java
@@ -44,4 +44,14 @@ public class NaiveDelegationStore implements DelegationStore {
     public Publisher<Void> removeAuthorizedUser(Username baseUser, Username userWithAccess) {
         throw new NotImplementedException();
     }
+
+    @Override
+    public Publisher<Username> delegatedUsers(Username baseUser) {
+        throw new NotImplementedException();
+    }
+
+    @Override
+    public Publisher<Void> removeDelegatedUser(Username baseUser, Username delegatedToUser) {
+        throw new NotImplementedException();
+    }
 }
diff --git a/upgrade-instructions.md b/upgrade-instructions.md
index 545238a009..e908deeb96 100644
--- a/upgrade-instructions.md
+++ b/upgrade-instructions.md
@@ -25,6 +25,7 @@ Change list:
 - [Blob Store AES upgraded to PBKDF2WithHmacSHA512](#blob-store-aes-upgraded-to-pbkdf2withhmacsha512)
 - [Adding saveDate column to messageIdTable and imapUidTable](#adding-savedate-column-to-messageidtable-and-imapuidtable)
 - [Adding the saveDate to the OpenSearch index](#adding-the-savedate-to-the-opensearch-index)
+- [Adding delegatedUser column to user_table](#adding-delegatedusers-column-to-user-table)
 
 ### Adding the saveDate to the OpenSearch index
 
@@ -67,6 +68,21 @@ ALTER TABLE james_keyspace.messageIdTable ADD save_date timestamp;
 ALTER TABLE james_keyspace.imapUidTable ADD save_date timestamp;
 ```
 
+### Adding delegated_users column to user table
+
+Date 05/01/2023
+
+JIRA: https://issues.apache.org/jira/browse/JAMES-3756
+
+Concerned product: Distributed James, Cassandra James Server
+
+Add `delegated_users` column to `user` tables in order to store delegated users that user has access to.
+
+In order to add this `delegated_users` column you need to run the following CQL commands:
+```
+ALTER TABLE james_keyspace.user ADD delegated_users set<text>;
+```
+
 ### Blob Store AES upgraded to PBKDF2WithHmacSHA512
 
 Date: 06/10/2022


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org