From ffdc14b5f20eb125e4cd14b620965b01b0e2deff Mon Sep 17 00:00:00 2001 From: hungphan227 <45198168+hungphan227@users.noreply.github.com> Date: Wed, 13 Dec 2023 16:24:11 +0700 Subject: [PATCH] JAMES-2586 PostgresDelegationStore (#1851) --- .../apache/james/PostgresJamesServerMain.java | 3 +- .../data/PostgresDelegationStoreModule.java | 62 ++++++++++++ .../data/PostgresUsersRepositoryModule.java | 22 ----- .../postgres/PostgresDelegationStore.java | 89 +++++++++++++++++ .../user/postgres/PostgresUserModule.java | 8 +- .../james/user/postgres/PostgresUsersDAO.java | 98 +++++++++++++++++++ .../postgres/PostgresDelegationStoreTest.java | 67 +++++++++++++ 7 files changed, 324 insertions(+), 25 deletions(-) create mode 100644 server/container/guice/postgres-common/src/main/java/org/apache/james/modules/data/PostgresDelegationStoreModule.java create mode 100644 server/data/data-postgres/src/main/java/org/apache/james/user/postgres/PostgresDelegationStore.java create mode 100644 server/data/data-postgres/src/test/java/org/apache/james/user/postgres/PostgresDelegationStoreTest.java diff --git a/server/apps/postgres-app/src/main/java/org/apache/james/PostgresJamesServerMain.java b/server/apps/postgres-app/src/main/java/org/apache/james/PostgresJamesServerMain.java index 1191382350f..24cfa7d1cd3 100644 --- a/server/apps/postgres-app/src/main/java/org/apache/james/PostgresJamesServerMain.java +++ b/server/apps/postgres-app/src/main/java/org/apache/james/PostgresJamesServerMain.java @@ -24,6 +24,7 @@ import org.apache.james.modules.MailetProcessingModule; import org.apache.james.modules.RunArgumentsModule; import org.apache.james.modules.data.PostgresDataModule; +import org.apache.james.modules.data.PostgresDelegationStoreModule; import org.apache.james.modules.data.PostgresUsersRepositoryModule; import org.apache.james.modules.data.SievePostgresRepositoryModules; import org.apache.james.modules.mailbox.DefaultEventModule; @@ -79,7 +80,7 @@ public class PostgresJamesServerMain implements JamesServerMain { private static final Module POSTGRES_SERVER_MODULE = Modules.combine( new ActiveMQQueueModule(), - new NaiveDelegationStoreModule(), + new PostgresDelegationStoreModule(), new DefaultProcessorsConfigurationProviderModule(), new PostgresMailboxModule(), new PostgresDataModule(), diff --git a/server/container/guice/postgres-common/src/main/java/org/apache/james/modules/data/PostgresDelegationStoreModule.java b/server/container/guice/postgres-common/src/main/java/org/apache/james/modules/data/PostgresDelegationStoreModule.java new file mode 100644 index 00000000000..f6e5521ead7 --- /dev/null +++ b/server/container/guice/postgres-common/src/main/java/org/apache/james/modules/data/PostgresDelegationStoreModule.java @@ -0,0 +1,62 @@ +/**************************************************************** + * 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.modules.data; + +import org.apache.commons.configuration2.ex.ConfigurationException; +import org.apache.james.backends.postgres.PostgresModule; +import org.apache.james.server.core.configuration.ConfigurationProvider; +import org.apache.james.user.api.DelegationStore; +import org.apache.james.user.api.DelegationUsernameChangeTaskStep; +import org.apache.james.user.api.UsernameChangeTaskStep; +import org.apache.james.user.lib.UsersDAO; +import org.apache.james.user.postgres.PostgresDelegationStore; +import org.apache.james.user.postgres.PostgresUserModule; +import org.apache.james.user.postgres.PostgresUsersDAO; +import org.apache.james.user.postgres.PostgresUsersRepositoryConfiguration; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Scopes; +import com.google.inject.Singleton; +import com.google.inject.multibindings.Multibinder; + +public class PostgresDelegationStoreModule extends AbstractModule { + @Override + public void configure() { + bind(DelegationStore.class).to(PostgresDelegationStore.class); + bind(PostgresDelegationStore.UserExistencePredicate.class).to(PostgresDelegationStore.UserExistencePredicateImplementation.class); + + Multibinder.newSetBinder(binder(), UsernameChangeTaskStep.class) + .addBinding().to(DelegationUsernameChangeTaskStep.class); + + bind(PostgresUsersDAO.class).in(Scopes.SINGLETON); + bind(UsersDAO.class).to(PostgresUsersDAO.class); + + Multibinder postgresDataDefinitions = Multibinder.newSetBinder(binder(), PostgresModule.class); + postgresDataDefinitions.addBinding().toInstance(PostgresUserModule.MODULE); + } + + @Provides + @Singleton + public PostgresUsersRepositoryConfiguration provideConfiguration(ConfigurationProvider configurationProvider) throws ConfigurationException { + return PostgresUsersRepositoryConfiguration.from( + configurationProvider.getConfiguration("usersrepository")); + } +} diff --git a/server/container/guice/postgres-common/src/main/java/org/apache/james/modules/data/PostgresUsersRepositoryModule.java b/server/container/guice/postgres-common/src/main/java/org/apache/james/modules/data/PostgresUsersRepositoryModule.java index 575f7621f0d..ff30223bb8c 100644 --- a/server/container/guice/postgres-common/src/main/java/org/apache/james/modules/data/PostgresUsersRepositoryModule.java +++ b/server/container/guice/postgres-common/src/main/java/org/apache/james/modules/data/PostgresUsersRepositoryModule.java @@ -19,23 +19,14 @@ package org.apache.james.modules.data; -import org.apache.commons.configuration2.ex.ConfigurationException; -import org.apache.james.backends.postgres.PostgresModule; import org.apache.james.server.core.configuration.ConfigurationProvider; import org.apache.james.user.api.UsersRepository; -import org.apache.james.user.lib.UsersDAO; -import org.apache.james.user.postgres.PostgresUserModule; -import org.apache.james.user.postgres.PostgresUsersDAO; import org.apache.james.user.postgres.PostgresUsersRepository; -import org.apache.james.user.postgres.PostgresUsersRepositoryConfiguration; import org.apache.james.utils.InitializationOperation; import org.apache.james.utils.InitilizationOperationBuilder; import com.google.inject.AbstractModule; -import com.google.inject.Provides; import com.google.inject.Scopes; -import com.google.inject.Singleton; -import com.google.inject.multibindings.Multibinder; import com.google.inject.multibindings.ProvidesIntoSet; public class PostgresUsersRepositoryModule extends AbstractModule { @@ -43,19 +34,6 @@ public class PostgresUsersRepositoryModule extends AbstractModule { public void configure() { bind(PostgresUsersRepository.class).in(Scopes.SINGLETON); bind(UsersRepository.class).to(PostgresUsersRepository.class); - - bind(PostgresUsersDAO.class).in(Scopes.SINGLETON); - bind(UsersDAO.class).to(PostgresUsersDAO.class); - - Multibinder postgresDataDefinitions = Multibinder.newSetBinder(binder(), PostgresModule.class); - postgresDataDefinitions.addBinding().toInstance(PostgresUserModule.MODULE); - } - - @Provides - @Singleton - public PostgresUsersRepositoryConfiguration provideConfiguration(ConfigurationProvider configurationProvider) throws ConfigurationException { - return PostgresUsersRepositoryConfiguration.from( - configurationProvider.getConfiguration("usersrepository")); } @ProvidesIntoSet diff --git a/server/data/data-postgres/src/main/java/org/apache/james/user/postgres/PostgresDelegationStore.java b/server/data/data-postgres/src/main/java/org/apache/james/user/postgres/PostgresDelegationStore.java new file mode 100644 index 00000000000..4f04f450752 --- /dev/null +++ b/server/data/data-postgres/src/main/java/org/apache/james/user/postgres/PostgresDelegationStore.java @@ -0,0 +1,89 @@ +/**************************************************************** + * 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.user.postgres; + +import javax.inject.Inject; + +import org.apache.james.core.Username; +import org.apache.james.user.api.DelegationStore; +import org.apache.james.user.api.UsersRepository; +import org.reactivestreams.Publisher; + +import reactor.core.publisher.Mono; + +public class PostgresDelegationStore implements DelegationStore { + public interface UserExistencePredicate { + Mono exists(Username username); + } + + public static class UserExistencePredicateImplementation implements UserExistencePredicate { + private final UsersRepository usersRepository; + + @Inject + UserExistencePredicateImplementation(UsersRepository usersRepository) { + this.usersRepository = usersRepository; + } + + @Override + public Mono exists(Username username) { + return Mono.from(usersRepository.containsReactive(username)); + } + } + + private PostgresUsersDAO postgresUsersDAO; + private final UserExistencePredicate userExistencePredicate; + + @Inject + public PostgresDelegationStore(PostgresUsersDAO postgresUsersDAO, UserExistencePredicate userExistencePredicate) { + this.postgresUsersDAO = postgresUsersDAO; + this.userExistencePredicate = userExistencePredicate; + } + + @Override + public Publisher authorizedUsers(Username baseUser) { + return postgresUsersDAO.getAuthorizedUsers(baseUser); + } + + @Override + public Publisher clear(Username baseUser) { + return postgresUsersDAO.removeAllAuthorizedUsers(baseUser); + } + + @Override + public Publisher addAuthorizedUser(Username baseUser, Username userWithAccess) { + return userExistencePredicate.exists(userWithAccess) + .flatMap(targetUserExists -> postgresUsersDAO.addAuthorizedUser(baseUser, userWithAccess, targetUserExists)); + } + + @Override + public Publisher removeAuthorizedUser(Username baseUser, Username userWithAccess) { + return postgresUsersDAO.removeAuthorizedUser(baseUser, userWithAccess); + } + + @Override + public Publisher delegatedUsers(Username baseUser) { + return postgresUsersDAO.getDelegatedToUsers(baseUser); + } + + @Override + public Publisher removeDelegatedUser(Username baseUser, Username delegatedToUser) { + return postgresUsersDAO.removeDelegatedToUser(baseUser, delegatedToUser); + } +} diff --git a/server/data/data-postgres/src/main/java/org/apache/james/user/postgres/PostgresUserModule.java b/server/data/data-postgres/src/main/java/org/apache/james/user/postgres/PostgresUserModule.java index 6aae9183f82..e5bc618d31d 100644 --- a/server/data/data-postgres/src/main/java/org/apache/james/user/postgres/PostgresUserModule.java +++ b/server/data/data-postgres/src/main/java/org/apache/james/user/postgres/PostgresUserModule.java @@ -32,14 +32,18 @@ interface PostgresUserTable { Table TABLE_NAME = DSL.table("users"); Field USERNAME = DSL.field("username", SQLDataType.VARCHAR(255).notNull()); - Field HASHED_PASSWORD = DSL.field("hashed_password", SQLDataType.VARCHAR.notNull()); - Field ALGORITHM = DSL.field("algorithm", SQLDataType.VARCHAR(100).notNull()); + Field HASHED_PASSWORD = DSL.field("hashed_password", SQLDataType.VARCHAR); + Field ALGORITHM = DSL.field("algorithm", SQLDataType.VARCHAR(100)); + Field AUTHORIZED_USERS = DSL.field("authorized_users", SQLDataType.VARCHAR.getArrayDataType()); + Field DELEGATED_USERS = DSL.field("delegated_users", SQLDataType.VARCHAR.getArrayDataType()); PostgresTable TABLE = PostgresTable.name(TABLE_NAME.getName()) .createTableStep(((dsl, tableName) -> dsl.createTableIfNotExists(tableName) .column(USERNAME) .column(HASHED_PASSWORD) .column(ALGORITHM) + .column(AUTHORIZED_USERS) + .column(DELEGATED_USERS) .constraint(DSL.primaryKey(USERNAME)))) .disableRowLevelSecurity(); } diff --git a/server/data/data-postgres/src/main/java/org/apache/james/user/postgres/PostgresUsersDAO.java b/server/data/data-postgres/src/main/java/org/apache/james/user/postgres/PostgresUsersDAO.java index d8447e527fb..d0467bf847f 100644 --- a/server/data/data-postgres/src/main/java/org/apache/james/user/postgres/PostgresUsersDAO.java +++ b/server/data/data-postgres/src/main/java/org/apache/james/user/postgres/PostgresUsersDAO.java @@ -22,7 +22,10 @@ import static org.apache.james.backends.postgres.utils.PostgresExecutor.DEFAULT_INJECT; import static org.apache.james.backends.postgres.utils.PostgresUtils.UNIQUE_CONSTRAINT_VIOLATION_PREDICATE; import static org.apache.james.user.postgres.PostgresUserModule.PostgresUserTable.ALGORITHM; +import static org.apache.james.user.postgres.PostgresUserModule.PostgresUserTable.AUTHORIZED_USERS; +import static org.apache.james.user.postgres.PostgresUserModule.PostgresUserTable.DELEGATED_USERS; import static org.apache.james.user.postgres.PostgresUserModule.PostgresUserTable.HASHED_PASSWORD; +import static org.apache.james.user.postgres.PostgresUserModule.PostgresUserTable.TABLE; import static org.apache.james.user.postgres.PostgresUserModule.PostgresUserTable.TABLE_NAME; import static org.apache.james.user.postgres.PostgresUserModule.PostgresUserTable.USERNAME; import static org.jooq.impl.DSL.count; @@ -41,8 +44,14 @@ import org.apache.james.user.lib.UsersDAO; import org.apache.james.user.lib.model.Algorithm; import org.apache.james.user.lib.model.DefaultUser; +import org.jooq.DSLContext; +import org.jooq.Field; +import org.jooq.Record; +import org.jooq.UpdateConditionStep; +import org.jooq.impl.DSL; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -141,4 +150,93 @@ public void addUser(Username username, String password) { e -> new AlreadyExistInUsersRepositoryException("User with username " + username + " already exist!")) .block(); } + + public Mono addAuthorizedUser(Username baseUser, Username userWithAccess, boolean targetUserExists) { + return addUserToList(AUTHORIZED_USERS, baseUser, userWithAccess) + .then(addDelegatedUser(baseUser, userWithAccess, targetUserExists)); + } + + private Mono addDelegatedUser(Username baseUser, Username userWithAccess, boolean targetUserExists) { + if (targetUserExists) { + return addUserToList(DELEGATED_USERS, userWithAccess, baseUser); + } else { + return Mono.empty(); + } + } + + private Mono addUserToList(Field field, Username baseUser, Username targetUser) { + String fullAuthorizedUsersColumnName = TABLE.getName() + "." + field.getName(); + return postgresExecutor.executeVoid(dslContext -> + Mono.from(dslContext.insertInto(TABLE_NAME) + .set(USERNAME, baseUser.asString()) + .set(field, DSL.array(targetUser.asString())) + .onConflict(USERNAME) + .doUpdate() + .set(DSL.field(field.getName()), + (Object) DSL.field("array_append(coalesce(" + fullAuthorizedUsersColumnName + ", array[]::varchar[]), ?)", + targetUser.asString())) + .where(DSL.field(fullAuthorizedUsersColumnName).isNull() + .or(DSL.field(fullAuthorizedUsersColumnName).notContains(new String[]{targetUser.asString()}))))); + } + + public Mono removeAuthorizedUser(Username baseUser, Username userWithAccess) { + return removeUserInAuthorizedList(baseUser, userWithAccess) + .then(removeUserInDelegatedList(userWithAccess, baseUser)); + } + + public Mono removeDelegatedToUser(Username baseUser, Username delegatedToUser) { + return removeUserInDelegatedList(baseUser, delegatedToUser) + .then(removeUserInAuthorizedList(delegatedToUser, baseUser)); + } + + private Mono removeUserInAuthorizedList(Username baseUser, Username targetUser) { + return removeUserFromList(AUTHORIZED_USERS, baseUser, targetUser); + } + + private Mono removeUserInDelegatedList(Username baseUser, Username targetUser) { + return removeUserFromList(DELEGATED_USERS, baseUser, targetUser); + } + + private Mono removeUserFromList(Field field, Username baseUser, Username targetUser) { + return postgresExecutor.executeVoid(dslContext -> + Mono.from(createQueryRemoveUserFromList(dslContext, field, baseUser, targetUser))); + } + + private UpdateConditionStep createQueryRemoveUserFromList(DSLContext dslContext, Field field, Username baseUser, Username targetUser) { + return dslContext.update(TABLE_NAME) + .set(DSL.field(field.getName()), + (Object) DSL.field("array_remove(" + field.getName() + ", ?)", + targetUser.asString())) + .where(USERNAME.eq(baseUser.asString())) + .and(DSL.field(field.getName()).isNotNull()); + } + + public Mono removeAllAuthorizedUsers(Username baseUser) { + return getAuthorizedUsers(baseUser) + .collect(ImmutableList.toImmutableList()) + .flatMap(usernames -> postgresExecutor.executeVoid(dslContext -> + Mono.from(dslContext.batch(usernames.stream() + .map(username -> createQueryRemoveUserFromList(dslContext, DELEGATED_USERS, username, baseUser)) + .collect(ImmutableList.toImmutableList()))))) + .then(postgresExecutor.executeVoid(dslContext -> Mono.from(dslContext.update(TABLE_NAME) + .setNull(AUTHORIZED_USERS) + .where(USERNAME.eq(baseUser.asString()))))); + } + + public Flux getAuthorizedUsers(Username name) { + return getUsersFromList(AUTHORIZED_USERS, name); + } + + public Flux getDelegatedToUsers(Username name) { + return getUsersFromList(DELEGATED_USERS, name); + } + + public Flux getUsersFromList(Field field, Username name) { + return postgresExecutor.executeRow(dslContext -> Mono.from(dslContext.select(field) + .from(TABLE_NAME) + .where(USERNAME.eq(name.asString())))) + .flatMapMany(record -> Optional.ofNullable(record.get(field)) + .map(Flux::fromArray).orElse(Flux.empty())) + .map(Username::of); + } } diff --git a/server/data/data-postgres/src/test/java/org/apache/james/user/postgres/PostgresDelegationStoreTest.java b/server/data/data-postgres/src/test/java/org/apache/james/user/postgres/PostgresDelegationStoreTest.java new file mode 100644 index 00000000000..cae65185a65 --- /dev/null +++ b/server/data/data-postgres/src/test/java/org/apache/james/user/postgres/PostgresDelegationStoreTest.java @@ -0,0 +1,67 @@ +/**************************************************************** + * 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.user.postgres; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.james.backends.postgres.PostgresExtension; +import org.apache.james.core.Username; +import org.apache.james.user.api.DelegationStore; +import org.apache.james.user.api.DelegationStoreContract; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import reactor.core.publisher.Mono; + +public class PostgresDelegationStoreTest implements DelegationStoreContract { + @RegisterExtension + static PostgresExtension postgresExtension = PostgresExtension.withoutRowLevelSecurity(PostgresUserModule.MODULE); + + private PostgresUsersDAO postgresUsersDAO; + private PostgresDelegationStore postgresDelegationStore; + + @BeforeEach + void beforeEach() { + postgresUsersDAO = new PostgresUsersDAO(postgresExtension.getPostgresExecutor(), PostgresUsersRepositoryConfiguration.DEFAULT); + postgresDelegationStore = new PostgresDelegationStore(postgresUsersDAO, any -> Mono.just(true)); + } + + @Override + public DelegationStore testee() { + return postgresDelegationStore; + } + + @Override + public void addUser(Username username) { + postgresUsersDAO.addUser(username, "password"); + } + + @Test + void virtualUsersShouldNotBeListed() { + postgresDelegationStore = new PostgresDelegationStore(postgresUsersDAO, any -> Mono.just(false)); + addUser(BOB); + + Mono.from(testee().addAuthorizedUser(ALICE).forUser(BOB)).block(); + + assertThat(postgresUsersDAO.listReactive().collectList().block()) + .containsOnly(BOB); + } +}