This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch postgresql in repository https://gitbox.apache.org/repos/asf/james-project.git
The following commit(s) were added to refs/heads/postgresql by this push: new decec6ffc5 JAMES-2586 PostgresDelegationStore (#1851) decec6ffc5 is described below commit decec6ffc5ac8f5037572d47a099e257ed644029 Author: hungphan227 <45198168+hungphan...@users.noreply.github.com> AuthorDate: Wed Dec 13 16:24:11 2023 +0700 JAMES-2586 PostgresDelegationStore (#1851) --- .../org/apache/james/PostgresJamesServerMain.java | 3 +- ...ule.java => PostgresDelegationStoreModule.java} | 25 +++--- .../data/PostgresUsersRepositoryModule.java | 22 ----- .../user/postgres/PostgresDelegationStore.java | 89 ++++++++++++++++++++ .../james/user/postgres/PostgresUserModule.java | 8 +- .../james/user/postgres/PostgresUsersDAO.java | 98 ++++++++++++++++++++++ .../user/postgres/PostgresDelegationStoreTest.java | 67 +++++++++++++++ 7 files changed, 272 insertions(+), 40 deletions(-) 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 1191382350..24cfa7d1cd 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.MailboxModule; 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/PostgresUsersRepositoryModule.java b/server/container/guice/postgres-common/src/main/java/org/apache/james/modules/data/PostgresDelegationStoreModule.java similarity index 74% copy from server/container/guice/postgres-common/src/main/java/org/apache/james/modules/data/PostgresUsersRepositoryModule.java copy to server/container/guice/postgres-common/src/main/java/org/apache/james/modules/data/PostgresDelegationStoreModule.java index 575f7621f0..f6e5521ead 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/PostgresDelegationStoreModule.java @@ -22,27 +22,29 @@ 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.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.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 { +public class PostgresDelegationStoreModule extends AbstractModule { @Override public void configure() { - bind(PostgresUsersRepository.class).in(Scopes.SINGLETON); - bind(UsersRepository.class).to(PostgresUsersRepository.class); + 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); @@ -57,11 +59,4 @@ public class PostgresUsersRepositoryModule extends AbstractModule { return PostgresUsersRepositoryConfiguration.from( configurationProvider.getConfiguration("usersrepository")); } - - @ProvidesIntoSet - InitializationOperation configureInitialization(ConfigurationProvider configurationProvider, PostgresUsersRepository usersRepository) { - return InitilizationOperationBuilder - .forClass(PostgresUsersRepository.class) - .init(() -> usersRepository.configure(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 575f7621f0..ff30223bb8 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<PostgresModule> 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 0000000000..4f04f45075 --- /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<Boolean> exists(Username username); + } + + public static class UserExistencePredicateImplementation implements UserExistencePredicate { + private final UsersRepository usersRepository; + + @Inject + UserExistencePredicateImplementation(UsersRepository usersRepository) { + this.usersRepository = usersRepository; + } + + @Override + public Mono<Boolean> 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<Username> authorizedUsers(Username baseUser) { + return postgresUsersDAO.getAuthorizedUsers(baseUser); + } + + @Override + public Publisher<Void> clear(Username baseUser) { + return postgresUsersDAO.removeAllAuthorizedUsers(baseUser); + } + + @Override + public Publisher<Void> addAuthorizedUser(Username baseUser, Username userWithAccess) { + return userExistencePredicate.exists(userWithAccess) + .flatMap(targetUserExists -> postgresUsersDAO.addAuthorizedUser(baseUser, userWithAccess, targetUserExists)); + } + + @Override + public Publisher<Void> removeAuthorizedUser(Username baseUser, Username userWithAccess) { + return postgresUsersDAO.removeAuthorizedUser(baseUser, userWithAccess); + } + + @Override + public Publisher<Username> delegatedUsers(Username baseUser) { + return postgresUsersDAO.getDelegatedToUsers(baseUser); + } + + @Override + public Publisher<Void> 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 6aae9183f8..e5bc618d31 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 @@ public interface PostgresUserModule { Table<Record> TABLE_NAME = DSL.table("users"); Field<String> USERNAME = DSL.field("username", SQLDataType.VARCHAR(255).notNull()); - Field<String> HASHED_PASSWORD = DSL.field("hashed_password", SQLDataType.VARCHAR.notNull()); - Field<String> ALGORITHM = DSL.field("algorithm", SQLDataType.VARCHAR(100).notNull()); + Field<String> HASHED_PASSWORD = DSL.field("hashed_password", SQLDataType.VARCHAR); + Field<String> ALGORITHM = DSL.field("algorithm", SQLDataType.VARCHAR(100)); + Field<String[]> AUTHORIZED_USERS = DSL.field("authorized_users", SQLDataType.VARCHAR.getArrayDataType()); + Field<String[]> 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 d8447e527f..d0467bf847 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 @@ package org.apache.james.user.postgres; 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.api.model.User; 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 class PostgresUsersDAO implements UsersDAO { e -> new AlreadyExistInUsersRepositoryException("User with username " + username + " already exist!")) .block(); } + + public Mono<Void> addAuthorizedUser(Username baseUser, Username userWithAccess, boolean targetUserExists) { + return addUserToList(AUTHORIZED_USERS, baseUser, userWithAccess) + .then(addDelegatedUser(baseUser, userWithAccess, targetUserExists)); + } + + private Mono<Void> addDelegatedUser(Username baseUser, Username userWithAccess, boolean targetUserExists) { + if (targetUserExists) { + return addUserToList(DELEGATED_USERS, userWithAccess, baseUser); + } else { + return Mono.empty(); + } + } + + private Mono<Void> addUserToList(Field<String[]> 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<Void> removeAuthorizedUser(Username baseUser, Username userWithAccess) { + return removeUserInAuthorizedList(baseUser, userWithAccess) + .then(removeUserInDelegatedList(userWithAccess, baseUser)); + } + + public Mono<Void> removeDelegatedToUser(Username baseUser, Username delegatedToUser) { + return removeUserInDelegatedList(baseUser, delegatedToUser) + .then(removeUserInAuthorizedList(delegatedToUser, baseUser)); + } + + private Mono<Void> removeUserInAuthorizedList(Username baseUser, Username targetUser) { + return removeUserFromList(AUTHORIZED_USERS, baseUser, targetUser); + } + + private Mono<Void> removeUserInDelegatedList(Username baseUser, Username targetUser) { + return removeUserFromList(DELEGATED_USERS, baseUser, targetUser); + } + + private Mono<Void> removeUserFromList(Field<String[]> field, Username baseUser, Username targetUser) { + return postgresExecutor.executeVoid(dslContext -> + Mono.from(createQueryRemoveUserFromList(dslContext, field, baseUser, targetUser))); + } + + private UpdateConditionStep<Record> createQueryRemoveUserFromList(DSLContext dslContext, Field<String[]> 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<Void> 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<Username> getAuthorizedUsers(Username name) { + return getUsersFromList(AUTHORIZED_USERS, name); + } + + public Flux<Username> getDelegatedToUsers(Username name) { + return getUsersFromList(DELEGATED_USERS, name); + } + + public Flux<Username> getUsersFromList(Field<String[]> 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 0000000000..cae65185a6 --- /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); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@james.apache.org For additional commands, e-mail: notifications-h...@james.apache.org