Skip to content

Commit

Permalink
JAMES-2586 PostgresDelegationStore (linagora#1851)
Browse files Browse the repository at this point in the history
  • Loading branch information
hungphan227 authored and hung phan committed Nov 21, 2024
1 parent 2515a77 commit bb5b110
Show file tree
Hide file tree
Showing 7 changed files with 324 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
@@ -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<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"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,43 +19,21 @@

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 {
@Override
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,18 @@ interface PostgresUserTable {
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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -141,4 +150,93 @@ public void addUser(Username username, String password) {
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);
}
}
Loading

0 comments on commit bb5b110

Please sign in to comment.