This is an automated email from the ASF dual-hosted git repository. quantranhong1999 pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 4ee82ec470b6d1db581fc7c0dd82d6e1a246eb33 Author: Quan Tran <[email protected]> AuthorDate: Thu Jul 2 16:25:58 2026 +0700 JAMES-4195 Add Guice bindings for JMAP OIDC caches Adds Guice modules for JMAP OIDC resolver/cache binding, Redis cache binding, default James Redis key prefix, optional cache implementation selection, and optional app wiring. --- pom.xml | 10 +++ server/apps/distributed-app/pom.xml | 4 + .../james/CassandraRabbitMQJamesConfiguration.java | 51 ++++++++++++- .../james/CassandraRabbitMQJamesServerMain.java | 14 ++++ .../org/apache/james/MemoryJamesConfiguration.java | 29 +++++++- .../org/apache/james/MemoryJamesServerMain.java | 14 ++++ server/apps/postgres-app/pom.xml | 4 + .../apache/james/PostgresJamesConfiguration.java | 54 ++++++++++++++ .../org/apache/james/PostgresJamesServerMain.java | 14 ++++ server/container/guice/pom.xml | 11 +++ .../container/guice/protocols/jmap-redis/pom.xml | 60 +++++++++++++++ .../oidc/redis/OidcTokenCacheModuleChooser.java | 63 ++++++++++++++++ .../redis/RedisOidcTokenCacheKeyPrefixModule.java | 30 ++++++++ .../jmap/oidc/redis/RedisOidcTokenCacheModule.java | 85 ++++++++++++++++++++++ .../jmap/oidc/CaffeineOidcTokenCacheModule.java | 31 ++++++++ .../org/apache/james/jmap/oidc/JMAPOidcModule.java | 66 +++++++++++++++++ 16 files changed, 537 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index e25e86fe5e..310f3af42e 100644 --- a/pom.xml +++ b/pom.xml @@ -1572,6 +1572,11 @@ <version>${project.version}</version> <type>test-jar</type> </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-guice-jmap-redis</artifactId> + <version>${project.version}</version> + </dependency> <dependency> <groupId>${james.groupId}</groupId> <artifactId>james-server-guice-mailbox</artifactId> @@ -1635,6 +1640,11 @@ <artifactId>james-server-jmap</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-jmap-redis</artifactId> + <version>${project.version}</version> + </dependency> <dependency> <groupId>${james.groupId}</groupId> <artifactId>james-server-jmap-rfc-8621</artifactId> diff --git a/server/apps/distributed-app/pom.xml b/server/apps/distributed-app/pom.xml index cca08e9bb6..24c8e343c8 100644 --- a/server/apps/distributed-app/pom.xml +++ b/server/apps/distributed-app/pom.xml @@ -198,6 +198,10 @@ <type>test-jar</type> <scope>test</scope> </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-guice-jmap-redis</artifactId> + </dependency> <dependency> <groupId>${james.groupId}</groupId> <artifactId>james-server-guice-jmx</artifactId> diff --git a/server/apps/distributed-app/src/main/java/org/apache/james/CassandraRabbitMQJamesConfiguration.java b/server/apps/distributed-app/src/main/java/org/apache/james/CassandraRabbitMQJamesConfiguration.java index c4b6436059..ce8a7906a7 100644 --- a/server/apps/distributed-app/src/main/java/org/apache/james/CassandraRabbitMQJamesConfiguration.java +++ b/server/apps/distributed-app/src/main/java/org/apache/james/CassandraRabbitMQJamesConfiguration.java @@ -28,6 +28,8 @@ import org.apache.james.data.UsersRepositoryModuleChooser; import org.apache.james.filesystem.api.FileSystem; import org.apache.james.filesystem.api.JamesDirectoriesProvider; import org.apache.james.jmap.JMAPModule; +import org.apache.james.jmap.oidc.JMAPOidcConfiguration; +import org.apache.james.jmap.oidc.redis.OidcTokenCacheModuleChooser; import org.apache.james.modules.blobstore.BlobStoreConfiguration; import org.apache.james.modules.queue.rabbitmq.MailQueueViewChoice; import org.apache.james.server.core.JamesServerResourceLoader; @@ -51,6 +53,8 @@ public class CassandraRabbitMQJamesConfiguration implements Configuration { private Optional<UsersRepositoryModuleChooser.Implementation> usersRepositoryImplementation; private Optional<VaultConfiguration> vaultConfiguration; private Optional<Boolean> jmapEnabled; + private Optional<Boolean> jmapOidcEnabled; + private Optional<OidcTokenCacheModuleChooser.Implementation> oidcTokenCacheImplementation; private Optional<Boolean> quotaCompatibilityMode; private Optional<Boolean> dropListsEnabled; @@ -64,6 +68,8 @@ public class CassandraRabbitMQJamesConfiguration implements Configuration { mailQueueChoice = Optional.empty(); vaultConfiguration = Optional.empty(); jmapEnabled = Optional.empty(); + jmapOidcEnabled = Optional.empty(); + oidcTokenCacheImplementation = Optional.empty(); quotaCompatibilityMode = Optional.empty(); dropListsEnabled = Optional.empty(); } @@ -131,6 +137,16 @@ public class CassandraRabbitMQJamesConfiguration implements Configuration { return this; } + public Builder enableJMAPOidc() { + this.jmapOidcEnabled = Optional.of(true); + return this; + } + + public Builder oidcTokenCacheImplementation(OidcTokenCacheModuleChooser.Implementation oidcTokenCacheImplementation) { + this.oidcTokenCacheImplementation = Optional.of(oidcTokenCacheImplementation); + return this; + } + public Builder quotaCompatibilityModeEnabled(boolean value) { this.quotaCompatibilityMode = Optional.of(value); return this; @@ -187,6 +203,23 @@ public class CassandraRabbitMQJamesConfiguration implements Configuration { } }); + boolean jmapOidcEnabled = this.jmapOidcEnabled.orElseGet(() -> { + try { + return JMAPOidcConfiguration.parseConfiguration(propertiesProvider).getOidcEnabled(); + } catch (FileNotFoundException e) { + return false; + } catch (ConfigurationException e) { + throw new RuntimeException(e); + } + }); + + OidcTokenCacheModuleChooser.Implementation oidcTokenCacheImplementation = this.oidcTokenCacheImplementation.orElseGet(() -> { + if (jmapOidcEnabled) { + return OidcTokenCacheModuleChooser.Implementation.from(propertiesProvider); + } + return OidcTokenCacheModuleChooser.Implementation.CAFFEINE; + }); + boolean quotaCompatibilityMode = this.quotaCompatibilityMode.orElseGet(() -> { try { return propertiesProvider.getConfiguration("cassandra").getBoolean("quota.compatibility.mode", false); @@ -216,6 +249,8 @@ public class CassandraRabbitMQJamesConfiguration implements Configuration { mailQueueChoice, mailQueueViewChoice, vaultConfiguration, jmapEnabled, + jmapOidcEnabled, + oidcTokenCacheImplementation, quotaCompatibilityMode, dropListsEnabled); } @@ -234,6 +269,8 @@ public class CassandraRabbitMQJamesConfiguration implements Configuration { private final MailQueueViewChoice mailQueueViewChoice; private final VaultConfiguration vaultConfiguration; private final boolean jmapEnabled; + private final boolean jmapOidcEnabled; + private final OidcTokenCacheModuleChooser.Implementation oidcTokenCacheImplementation; private final boolean quotaCompatibilityMode; private final boolean dropListsEnabled; @@ -241,7 +278,9 @@ public class CassandraRabbitMQJamesConfiguration implements Configuration { BlobStoreConfiguration blobStoreConfiguration, SearchConfiguration searchConfiguration, UsersRepositoryModuleChooser.Implementation usersRepositoryImplementation, MailQueueChoice mailQueueChoice, MailQueueViewChoice mailQueueViewChoice, VaultConfiguration vaultConfiguration, - boolean jmapEnabled, boolean quotaCompatibilityMode, boolean dropListsEnabled) { + boolean jmapEnabled, boolean jmapOidcEnabled, + OidcTokenCacheModuleChooser.Implementation oidcTokenCacheImplementation, + boolean quotaCompatibilityMode, boolean dropListsEnabled) { this.configurationPath = configurationPath; this.directories = directories; this.blobStoreConfiguration = blobStoreConfiguration; @@ -251,6 +290,8 @@ public class CassandraRabbitMQJamesConfiguration implements Configuration { this.mailQueueViewChoice = mailQueueViewChoice; this.vaultConfiguration = vaultConfiguration; this.jmapEnabled = jmapEnabled; + this.jmapOidcEnabled = jmapOidcEnabled; + this.oidcTokenCacheImplementation = oidcTokenCacheImplementation; this.quotaCompatibilityMode = quotaCompatibilityMode; this.dropListsEnabled = dropListsEnabled; } @@ -293,6 +334,14 @@ public class CassandraRabbitMQJamesConfiguration implements Configuration { return jmapEnabled; } + public boolean isJmapOidcEnabled() { + return jmapOidcEnabled; + } + + public OidcTokenCacheModuleChooser.Implementation oidcTokenCacheImplementation() { + return oidcTokenCacheImplementation; + } + public boolean isQuotaCompatibilityMode() { return quotaCompatibilityMode; } diff --git a/server/apps/distributed-app/src/main/java/org/apache/james/CassandraRabbitMQJamesServerMain.java b/server/apps/distributed-app/src/main/java/org/apache/james/CassandraRabbitMQJamesServerMain.java index 335c00faf8..84289b2547 100644 --- a/server/apps/distributed-app/src/main/java/org/apache/james/CassandraRabbitMQJamesServerMain.java +++ b/server/apps/distributed-app/src/main/java/org/apache/james/CassandraRabbitMQJamesServerMain.java @@ -26,6 +26,8 @@ import org.apache.james.data.UsersRepositoryModuleChooser; import org.apache.james.eventsourcing.eventstore.EventNestedTypes; import org.apache.james.jmap.JMAPListenerModule; import org.apache.james.jmap.JMAPModule; +import org.apache.james.jmap.oidc.JMAPOidcModule; +import org.apache.james.jmap.oidc.redis.OidcTokenCacheModuleChooser; import org.apache.james.json.DTO; import org.apache.james.json.DTOModule; import org.apache.james.modules.BlobExportMechanismModule; @@ -95,6 +97,7 @@ import org.apache.james.modules.server.VacationRoutesModule; import org.apache.james.modules.server.WebAdminMailOverWebModule; import org.apache.james.modules.server.WebAdminReIndexingTaskSerializationModule; import org.apache.james.modules.server.WebAdminServerModule; +import org.apache.james.modules.server.oidc.OidcBackchannelLogoutRoutesModule; import org.apache.james.modules.vault.DeletedMessageVaultRoutesModule; import org.apache.james.modules.webadmin.CassandraRoutesModule; import org.apache.james.modules.webadmin.InconsistencySolvingRoutesModule; @@ -216,6 +219,7 @@ public class CassandraRabbitMQJamesServerMain implements JamesServerMain { .chooseModules(configuration.getUsersRepositoryImplementation())) .combineWith(chooseDeletedMessageVault(configuration.getVaultConfiguration())) .combineWith(chooseQuotaModule(configuration)) + .combineWith(chooseJmapOidcModules(configuration)) .overrideWith(chooseJmapModules(configuration)) .overrideWith(chooseDropListsModule(configuration)); } @@ -254,6 +258,16 @@ public class CassandraRabbitMQJamesServerMain implements JamesServerMain { }; } + private static Module chooseJmapOidcModules(CassandraRabbitMQJamesConfiguration configuration) { + if (configuration.isJmapEnabled() && configuration.isJmapOidcEnabled()) { + return Modules.combine( + new JMAPOidcModule(), + OidcTokenCacheModuleChooser.chooseModules(configuration.oidcTokenCacheImplementation()), + new OidcBackchannelLogoutRoutesModule()); + } + return Modules.EMPTY_MODULE; + } + private static Module chooseQuotaModule(CassandraRabbitMQJamesConfiguration configuration) { if (configuration.isQuotaCompatibilityMode()) { return Modules.combine(new CassandraMailboxQuotaLegacyModule(), new CassandraSieveQuotaLegacyModule()); diff --git a/server/apps/memory-app/src/main/java/org/apache/james/MemoryJamesConfiguration.java b/server/apps/memory-app/src/main/java/org/apache/james/MemoryJamesConfiguration.java index aa0e08f3f1..82cb315171 100644 --- a/server/apps/memory-app/src/main/java/org/apache/james/MemoryJamesConfiguration.java +++ b/server/apps/memory-app/src/main/java/org/apache/james/MemoryJamesConfiguration.java @@ -28,6 +28,7 @@ import org.apache.james.data.UsersRepositoryModuleChooser; import org.apache.james.filesystem.api.FileSystem; import org.apache.james.filesystem.api.JamesDirectoriesProvider; import org.apache.james.jmap.JMAPModule; +import org.apache.james.jmap.oidc.JMAPOidcConfiguration; import org.apache.james.server.core.JamesServerResourceLoader; import org.apache.james.server.core.MissingArgumentException; import org.apache.james.server.core.configuration.Configuration; @@ -41,6 +42,7 @@ public class MemoryJamesConfiguration implements Configuration { private Optional<ConfigurationPath> configurationPath; private Optional<UsersRepositoryModuleChooser.Implementation> usersRepositoryImplementation; private Optional<Boolean> jmapEnabled; + private Optional<Boolean> jmapOidcEnabled; private Optional<Boolean> dropListsEnabled; private Builder() { @@ -48,6 +50,7 @@ public class MemoryJamesConfiguration implements Configuration { configurationPath = Optional.empty(); usersRepositoryImplementation = Optional.empty(); jmapEnabled = Optional.empty(); + jmapOidcEnabled = Optional.empty(); dropListsEnabled = Optional.empty(); } @@ -89,6 +92,11 @@ public class MemoryJamesConfiguration implements Configuration { return this; } + public Builder enableJMAPOidc() { + this.jmapOidcEnabled = Optional.of(true); + return this; + } + public Builder enableDropLists() { this.dropListsEnabled = Optional.of(true); return this; @@ -119,6 +127,17 @@ public class MemoryJamesConfiguration implements Configuration { } }); + boolean jmapOidcEnabled = this.jmapOidcEnabled.orElseGet(() -> { + PropertiesProvider propertiesProvider = new PropertiesProvider(fileSystem, configurationPath); + try { + return JMAPOidcConfiguration.parseConfiguration(propertiesProvider).getOidcEnabled(); + } catch (FileNotFoundException e) { + return false; + } catch (ConfigurationException e) { + throw new RuntimeException(e); + } + }); + boolean dropListsEnabled = this.dropListsEnabled.orElseGet(() -> { PropertiesProvider propertiesProvider = new PropertiesProvider(fileSystem, configurationPath); try { @@ -134,7 +153,7 @@ public class MemoryJamesConfiguration implements Configuration { return new MemoryJamesConfiguration( configurationPath, directories, - usersRepositoryChoice, jmapEnabled, dropListsEnabled); + usersRepositoryChoice, jmapEnabled, jmapOidcEnabled, dropListsEnabled); } } @@ -146,15 +165,17 @@ public class MemoryJamesConfiguration implements Configuration { private final JamesDirectoriesProvider directories; private final UsersRepositoryModuleChooser.Implementation usersRepositoryImplementation; private final boolean jmapEnabled; + private final boolean jmapOidcEnabled; private final boolean dropListsEnabled; public MemoryJamesConfiguration(ConfigurationPath configurationPath, JamesDirectoriesProvider directories, UsersRepositoryModuleChooser.Implementation usersRepositoryImplementation, - boolean jmapEnabled, boolean dropListsEnabled) { + boolean jmapEnabled, boolean jmapOidcEnabled, boolean dropListsEnabled) { this.configurationPath = configurationPath; this.directories = directories; this.usersRepositoryImplementation = usersRepositoryImplementation; this.jmapEnabled = jmapEnabled; + this.jmapOidcEnabled = jmapOidcEnabled; this.dropListsEnabled = dropListsEnabled; } @@ -176,6 +197,10 @@ public class MemoryJamesConfiguration implements Configuration { return jmapEnabled; } + public boolean isJmapOidcEnabled() { + return jmapOidcEnabled; + } + public boolean isDropListsEnabled() { return dropListsEnabled; } diff --git a/server/apps/memory-app/src/main/java/org/apache/james/MemoryJamesServerMain.java b/server/apps/memory-app/src/main/java/org/apache/james/MemoryJamesServerMain.java index 7c1b829ab4..4d21dd8a65 100644 --- a/server/apps/memory-app/src/main/java/org/apache/james/MemoryJamesServerMain.java +++ b/server/apps/memory-app/src/main/java/org/apache/james/MemoryJamesServerMain.java @@ -24,6 +24,8 @@ import org.apache.james.data.UsersRepositoryModuleChooser; import org.apache.james.jmap.JMAPListenerModule; import org.apache.james.jmap.JMAPModule; import org.apache.james.jmap.memory.pushsubscription.MemoryPushSubscriptionModule; +import org.apache.james.jmap.oidc.CaffeineOidcTokenCacheModule; +import org.apache.james.jmap.oidc.JMAPOidcModule; import org.apache.james.jwt.JwtConfiguration; import org.apache.james.modules.BlobExportMechanismModule; import org.apache.james.modules.BlobMemoryModule; @@ -67,6 +69,7 @@ import org.apache.james.modules.server.UserIdentityModule; import org.apache.james.modules.server.VacationRoutesModule; import org.apache.james.modules.server.WebAdminMailOverWebModule; import org.apache.james.modules.server.WebAdminServerModule; +import org.apache.james.modules.server.oidc.OidcBackchannelLogoutRoutesModule; import org.apache.james.modules.vault.DeletedMessageVaultModule; import org.apache.james.modules.vault.DeletedMessageVaultRoutesModule; import org.apache.james.webadmin.WebAdminConfiguration; @@ -175,6 +178,7 @@ public class MemoryJamesServerMain implements JamesServerMain { .combineWith(new UsersRepositoryModuleChooser(new MemoryUsersRepositoryModule()) .chooseModules(configuration.getUsersRepositoryImplementation())) .combineWith(chooseJmapModule(configuration)) + .combineWith(chooseJmapOidcModules(configuration)) .combineWith(chooseDropListsModule(configuration)); } @@ -187,6 +191,16 @@ public class MemoryJamesServerMain implements JamesServerMain { }; } + private static Module chooseJmapOidcModules(MemoryJamesConfiguration configuration) { + if (configuration.isJmapEnabled() && configuration.isJmapOidcEnabled()) { + return Modules.combine( + new JMAPOidcModule(), + new CaffeineOidcTokenCacheModule(), + new OidcBackchannelLogoutRoutesModule()); + } + return Modules.EMPTY_MODULE; + } + private static Module chooseDropListsModule(MemoryJamesConfiguration configuration) { if (configuration.isDropListsEnabled()) { return Modules.combine(new MemoryDropListsModule(), new DropListsRoutesModule()); diff --git a/server/apps/postgres-app/pom.xml b/server/apps/postgres-app/pom.xml index 84a08639c5..efe731000e 100644 --- a/server/apps/postgres-app/pom.xml +++ b/server/apps/postgres-app/pom.xml @@ -155,6 +155,10 @@ <type>test-jar</type> <scope>test</scope> </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-guice-jmap-redis</artifactId> + </dependency> <dependency> <groupId>${james.groupId}</groupId> <artifactId>james-server-guice-jmx</artifactId> diff --git a/server/apps/postgres-app/src/main/java/org/apache/james/PostgresJamesConfiguration.java b/server/apps/postgres-app/src/main/java/org/apache/james/PostgresJamesConfiguration.java index 21b9c633c7..dea88c56e0 100644 --- a/server/apps/postgres-app/src/main/java/org/apache/james/PostgresJamesConfiguration.java +++ b/server/apps/postgres-app/src/main/java/org/apache/james/PostgresJamesConfiguration.java @@ -29,6 +29,8 @@ import org.apache.james.data.UsersRepositoryModuleChooser; import org.apache.james.filesystem.api.FileSystem; import org.apache.james.filesystem.api.JamesDirectoriesProvider; import org.apache.james.jmap.JMAPModule; +import org.apache.james.jmap.oidc.JMAPOidcConfiguration; +import org.apache.james.jmap.oidc.redis.OidcTokenCacheModuleChooser; import org.apache.james.modules.blobstore.BlobStoreConfiguration; import org.apache.james.server.core.JamesServerResourceLoader; import org.apache.james.server.core.MissingArgumentException; @@ -75,6 +77,8 @@ public class PostgresJamesConfiguration implements Configuration { private Optional<EventBusImpl> eventBusImpl; private Optional<VaultConfiguration> deletedMessageVaultConfiguration; private Optional<Boolean> jmapEnabled; + private Optional<Boolean> jmapOidcEnabled; + private Optional<OidcTokenCacheModuleChooser.Implementation> oidcTokenCacheImplementation; private Optional<Boolean> dropListsEnabled; private Optional<Boolean> rlsEnabled; @@ -87,6 +91,8 @@ public class PostgresJamesConfiguration implements Configuration { eventBusImpl = Optional.empty(); deletedMessageVaultConfiguration = Optional.empty(); jmapEnabled = Optional.empty(); + jmapOidcEnabled = Optional.empty(); + oidcTokenCacheImplementation = Optional.empty(); dropListsEnabled = Optional.empty(); rlsEnabled = Optional.empty(); } @@ -149,6 +155,21 @@ public class PostgresJamesConfiguration implements Configuration { return this; } + public Builder jmapOidcEnabled(Optional<Boolean> jmapOidcEnabled) { + this.jmapOidcEnabled = jmapOidcEnabled; + return this; + } + + public Builder enableJMAPOidc() { + this.jmapOidcEnabled = Optional.of(true); + return this; + } + + public Builder oidcTokenCacheImplementation(OidcTokenCacheModuleChooser.Implementation oidcTokenCacheImplementation) { + this.oidcTokenCacheImplementation = Optional.of(oidcTokenCacheImplementation); + return this; + } + public Builder enableDropLists() { this.dropListsEnabled = Optional.of(true); return this; @@ -206,6 +227,23 @@ public class PostgresJamesConfiguration implements Configuration { } }); + boolean jmapOidcEnabled = this.jmapOidcEnabled.orElseGet(() -> { + try { + return JMAPOidcConfiguration.parseConfiguration(propertiesProvider).getOidcEnabled(); + } catch (FileNotFoundException e) { + return false; + } catch (ConfigurationException e) { + throw new RuntimeException(e); + } + }); + + OidcTokenCacheModuleChooser.Implementation oidcTokenCacheImplementation = this.oidcTokenCacheImplementation.orElseGet(() -> { + if (jmapOidcEnabled) { + return OidcTokenCacheModuleChooser.Implementation.from(propertiesProvider); + } + return OidcTokenCacheModuleChooser.Implementation.CAFFEINE; + }); + boolean dropListsEnabled = this.dropListsEnabled.orElseGet(() -> { try { return configurationProvider.getConfiguration("droplists").getBoolean("enabled", false); @@ -224,6 +262,8 @@ public class PostgresJamesConfiguration implements Configuration { eventBusImpl, deletedMessageVaultConfiguration, jmapEnabled, + jmapOidcEnabled, + oidcTokenCacheImplementation, dropListsEnabled, rlsEnabled); } @@ -251,6 +291,8 @@ public class PostgresJamesConfiguration implements Configuration { private final EventBusImpl eventBusImpl; private final VaultConfiguration deletedMessageVaultConfiguration; private final boolean jmapEnabled; + private final boolean jmapOidcEnabled; + private final OidcTokenCacheModuleChooser.Implementation oidcTokenCacheImplementation; private final boolean dropListsEnabled; private final boolean rlsEnabled; @@ -262,6 +304,8 @@ public class PostgresJamesConfiguration implements Configuration { EventBusImpl eventBusImpl, VaultConfiguration deletedMessageVaultConfiguration, boolean jmapEnabled, + boolean jmapOidcEnabled, + OidcTokenCacheModuleChooser.Implementation oidcTokenCacheImplementation, boolean dropListsEnabled, boolean rlsEnabled) { this.configurationPath = configurationPath; @@ -272,6 +316,8 @@ public class PostgresJamesConfiguration implements Configuration { this.eventBusImpl = eventBusImpl; this.deletedMessageVaultConfiguration = deletedMessageVaultConfiguration; this.jmapEnabled = jmapEnabled; + this.jmapOidcEnabled = jmapOidcEnabled; + this.oidcTokenCacheImplementation = oidcTokenCacheImplementation; this.dropListsEnabled = dropListsEnabled; this.rlsEnabled = rlsEnabled; } @@ -310,6 +356,14 @@ public class PostgresJamesConfiguration implements Configuration { return jmapEnabled; } + public boolean isJmapOidcEnabled() { + return jmapOidcEnabled; + } + + public OidcTokenCacheModuleChooser.Implementation oidcTokenCacheImplementation() { + return oidcTokenCacheImplementation; + } + public boolean isDropListsEnabled() { return dropListsEnabled; } 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 3095262e37..9e17ca2f61 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 @@ -29,6 +29,8 @@ import org.apache.james.data.UsersRepositoryModuleChooser; import org.apache.james.eventsourcing.eventstore.EventNestedTypes; import org.apache.james.jmap.JMAPListenerModule; import org.apache.james.jmap.JMAPModule; +import org.apache.james.jmap.oidc.JMAPOidcModule; +import org.apache.james.jmap.oidc.redis.OidcTokenCacheModuleChooser; import org.apache.james.json.DTO; import org.apache.james.json.DTOModule; import org.apache.james.modules.BlobExportMechanismModule; @@ -91,6 +93,7 @@ import org.apache.james.modules.server.TaskManagerModule; import org.apache.james.modules.server.UserIdentityModule; import org.apache.james.modules.server.WebAdminReIndexingTaskSerializationModule; import org.apache.james.modules.server.WebAdminServerModule; +import org.apache.james.modules.server.oidc.OidcBackchannelLogoutRoutesModule; import org.apache.james.modules.task.DistributedTaskManagerModule; import org.apache.james.modules.task.PostgresTaskExecutionDetailsProjectionGuiceModule; import org.apache.james.modules.vault.DeletedMessageVaultRoutesModule; @@ -197,6 +200,7 @@ public class PostgresJamesServerMain implements JamesServerMain { .combineWith(chooseBlobStoreModules(configuration)) .combineWith(chooseDeletedMessageVaultModules(configuration.getDeletedMessageVaultConfiguration())) .combineWith(chooseRLSSupportPostgresMailboxModule(configuration)) + .combineWith(chooseJmapOidcModules(configuration)) .overrideWith(chooseJmapModules(configuration)) .overrideWith(chooseTaskManagerModules(configuration)) .overrideWith(chooseDropListsModule(configuration)); @@ -267,6 +271,16 @@ public class PostgresJamesServerMain implements JamesServerMain { }; } + private static Module chooseJmapOidcModules(PostgresJamesConfiguration configuration) { + if (configuration.isJmapEnabled() && configuration.isJmapOidcEnabled()) { + return Modules.combine( + new JMAPOidcModule(), + OidcTokenCacheModuleChooser.chooseModules(configuration.oidcTokenCacheImplementation()), + new OidcBackchannelLogoutRoutesModule()); + } + return Modules.EMPTY_MODULE; + } + private static Module chooseDropListsModule(PostgresJamesConfiguration configuration) { if (configuration.isDropListsEnabled()) { return Modules.combine(new PostgresDropListsModule(), new DropListsRoutesModule()); diff --git a/server/container/guice/pom.xml b/server/container/guice/pom.xml index c9a917a728..aa1bed9826 100644 --- a/server/container/guice/pom.xml +++ b/server/container/guice/pom.xml @@ -62,6 +62,7 @@ <module>postgres-common</module> <module>protocols/imap</module> <module>protocols/jmap</module> + <module>protocols/jmap-redis</module> <module>protocols/lmtp</module> <module>protocols/managedsieve</module> <module>protocols/netty</module> @@ -135,6 +136,11 @@ <version>${project.version}</version> <type>test-jar</type> </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-guice-jmap-redis</artifactId> + <version>${project.version}</version> + </dependency> <dependency> <groupId>${james.groupId}</groupId> <artifactId>james-server-guice-jmx</artifactId> @@ -256,6 +262,11 @@ <artifactId>james-server-guice-webadmin-rabbitmq-mailqueue</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-jmap-redis</artifactId> + <version>${project.version}</version> + </dependency> <dependency> <groupId>${james.groupId}</groupId> <artifactId>james-server-jmap-rfc-8621</artifactId> diff --git a/server/container/guice/protocols/jmap-redis/pom.xml b/server/container/guice/protocols/jmap-redis/pom.xml new file mode 100644 index 0000000000..600ee45f0a --- /dev/null +++ b/server/container/guice/protocols/jmap-redis/pom.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.james</groupId> + <artifactId>james-server-guice</artifactId> + <version>3.10.0-SNAPSHOT</version> + <relativePath>../../pom.xml</relativePath> + </parent> + + <artifactId>james-server-guice-jmap-redis</artifactId> + + <name>Apache James :: Server :: Guice :: JMAP Redis</name> + <description>Redis related Guice bindings for JMAP RFC8621</description> + + <dependencies> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>apache-james-backends-redis</artifactId> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-guice-configuration</artifactId> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-guice-jmap</artifactId> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-jmap</artifactId> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-jmap-redis</artifactId> + </dependency> + <dependency> + <groupId>com.google.inject</groupId> + <artifactId>guice</artifactId> + </dependency> + </dependencies> +</project> diff --git a/server/container/guice/protocols/jmap-redis/src/main/java/org/apache/james/jmap/oidc/redis/OidcTokenCacheModuleChooser.java b/server/container/guice/protocols/jmap-redis/src/main/java/org/apache/james/jmap/oidc/redis/OidcTokenCacheModuleChooser.java new file mode 100644 index 0000000000..828ee21bac --- /dev/null +++ b/server/container/guice/protocols/jmap-redis/src/main/java/org/apache/james/jmap/oidc/redis/OidcTokenCacheModuleChooser.java @@ -0,0 +1,63 @@ +/**************************************************************** + * 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.jmap.oidc.redis; + +import java.io.FileNotFoundException; + +import org.apache.commons.configuration2.ex.ConfigurationException; +import org.apache.james.jmap.oidc.CaffeineOidcTokenCacheModule; +import org.apache.james.utils.PropertiesProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.Module; +import com.google.inject.util.Modules; + +public class OidcTokenCacheModuleChooser { + private static final Logger LOGGER = LoggerFactory.getLogger(OidcTokenCacheModuleChooser.class); + + public enum Implementation { + CAFFEINE, REDIS; + + public static Implementation from(PropertiesProvider propertiesProvider) { + try { + propertiesProvider.getConfiguration("redis"); + LOGGER.info("Redis configuration was found, using Redis OIDC token cache"); + return REDIS; + } catch (FileNotFoundException e) { + LOGGER.info("Redis configuration was not found, using in-memory OIDC token cache"); + return CAFFEINE; + } catch (ConfigurationException e) { + throw new RuntimeException(e); + } + } + } + + public static Module chooseModules(Implementation implementation) { + return chooseModules(implementation, new RedisOidcTokenCacheKeyPrefixModule()); + } + + public static Module chooseModules(Implementation implementation, Module redisOidcTokenCacheKeyPrefixModule) { + return switch (implementation) { + case CAFFEINE -> new CaffeineOidcTokenCacheModule(); + case REDIS -> Modules.combine(new RedisOidcTokenCacheModule(), redisOidcTokenCacheKeyPrefixModule); + }; + } +} diff --git a/server/container/guice/protocols/jmap-redis/src/main/java/org/apache/james/jmap/oidc/redis/RedisOidcTokenCacheKeyPrefixModule.java b/server/container/guice/protocols/jmap-redis/src/main/java/org/apache/james/jmap/oidc/redis/RedisOidcTokenCacheKeyPrefixModule.java new file mode 100644 index 0000000000..933341bedb --- /dev/null +++ b/server/container/guice/protocols/jmap-redis/src/main/java/org/apache/james/jmap/oidc/redis/RedisOidcTokenCacheKeyPrefixModule.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.jmap.oidc.redis; + +import com.google.inject.AbstractModule; + +public class RedisOidcTokenCacheKeyPrefixModule extends AbstractModule { + @Override + protected void configure() { + bind(RedisOidcTokenCacheKeyPrefix.class) + .toInstance(RedisOidcTokenCacheKeyPrefix.JAMES_DEFAULT); + } +} diff --git a/server/container/guice/protocols/jmap-redis/src/main/java/org/apache/james/jmap/oidc/redis/RedisOidcTokenCacheModule.java b/server/container/guice/protocols/jmap-redis/src/main/java/org/apache/james/jmap/oidc/redis/RedisOidcTokenCacheModule.java new file mode 100644 index 0000000000..35ddfd7473 --- /dev/null +++ b/server/container/guice/protocols/jmap-redis/src/main/java/org/apache/james/jmap/oidc/redis/RedisOidcTokenCacheModule.java @@ -0,0 +1,85 @@ +/**************************************************************** + * 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.jmap.oidc.redis; + +import java.io.FileNotFoundException; +import java.time.Duration; + +import org.apache.commons.configuration2.ex.ConfigurationException; +import org.apache.james.backends.redis.RedisConfiguration; +import org.apache.james.backends.redis.RedisReactiveCommandsFactory; +import org.apache.james.jmap.oidc.OidcTokenCache; +import org.apache.james.jmap.oidc.OidcTokenCacheConfiguration; +import org.apache.james.jmap.oidc.TokenInfoResolver; +import org.apache.james.utils.PropertiesProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Scopes; +import com.google.inject.Singleton; + +public class RedisOidcTokenCacheModule extends AbstractModule { + public static final Logger LOGGER = LoggerFactory.getLogger(RedisOidcTokenCacheModule.class); + + @Override + protected void configure() { + bind(OidcTokenCache.class).to(RedisOidcTokenCache.class) + .in(Scopes.SINGLETON); + } + + @Provides + @Singleton + public RedisOidcTokenCache provideRedisOIDCTokenCache(RedisReactiveCommandsFactory redisReactiveCommandsFactory, + OidcTokenCacheConfiguration oidcTokenCacheConfiguration, + RedisOidcTokenCacheConfiguration redisOidcTokenCacheConfiguration, + TokenInfoResolver tokenInfoResolver, + RedisOidcTokenCacheKeyPrefix keyPrefix) { + Duration commandTimeout = redisOidcTokenCacheConfiguration.commandTimeout(); + RedisTokenCacheCommands redisReactiveCommands = redisReactiveCommandsFactory.create( + commands -> RedisTokenCacheCommands.of(commands, commandTimeout), + commands -> RedisTokenCacheCommands.of(commands, commandTimeout)); + + return new RedisOidcTokenCache(tokenInfoResolver, oidcTokenCacheConfiguration, redisReactiveCommands, keyPrefix); + } + + @Provides + @Singleton + public RedisConfiguration redisConfiguration(PropertiesProvider propertiesProvider) throws ConfigurationException, FileNotFoundException { + try { + return RedisConfiguration.from(propertiesProvider.getConfiguration("redis")); + } catch (FileNotFoundException e) { + LOGGER.error("Missing `redis.properties` configuration file for Redis OIDC token cache usage."); + throw e; + } + } + + @Provides + @Singleton + public RedisOidcTokenCacheConfiguration redisOidcTokenCacheConfiguration(PropertiesProvider propertiesProvider) throws ConfigurationException { + try { + return RedisOidcTokenCacheConfiguration.from(propertiesProvider.getConfiguration("redis")); + } catch (FileNotFoundException e) { + LOGGER.info("Missing `redis.properties` configuration file -> using default RedisOidcTokenCacheConfiguration"); + return RedisOidcTokenCacheConfiguration.DEFAULT; + } + } +} diff --git a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/CaffeineOidcTokenCacheModule.java b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/CaffeineOidcTokenCacheModule.java new file mode 100644 index 0000000000..8db97ee87e --- /dev/null +++ b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/CaffeineOidcTokenCacheModule.java @@ -0,0 +1,31 @@ +/**************************************************************** + * 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.jmap.oidc; + +import com.google.inject.AbstractModule; +import com.google.inject.Scopes; + +public class CaffeineOidcTokenCacheModule extends AbstractModule { + @Override + protected void configure() { + bind(OidcTokenCache.class).to(CaffeineOidcTokenCache.class) + .in(Scopes.SINGLETON); + } +} diff --git a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/JMAPOidcModule.java b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/JMAPOidcModule.java new file mode 100644 index 0000000000..ddf89c8ce8 --- /dev/null +++ b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/oidc/JMAPOidcModule.java @@ -0,0 +1,66 @@ +/**************************************************************** + * 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.jmap.oidc; + +import java.io.FileNotFoundException; +import java.net.URL; +import java.util.List; + +import org.apache.commons.configuration2.ex.ConfigurationException; +import org.apache.james.jwt.introspection.IntrospectionEndpoint; +import org.apache.james.utils.PropertiesProvider; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import com.google.inject.name.Named; + +public class JMAPOidcModule extends AbstractModule { + @Override + protected void configure() { + bind(TokenInfoResolver.class).to(OidcEndpointsInfoResolver.class); + } + + @Provides + @Named("userInfo") + URL provideUserInfoEndpoint(JMAPOidcConfiguration configuration) { + return configuration.getOidcUserInfoUrl(); + } + + @Provides + IntrospectionEndpoint provideIntrospectionEndpoint(JMAPOidcConfiguration configuration) { + return configuration.getIntrospectionEndpoint(); + } + + @Provides + List<Aud> provideAudience(JMAPOidcConfiguration configuration) { + return configuration.getAud(); + } + + @Provides + @Singleton + OidcTokenCacheConfiguration oidcTokenCacheConfiguration(PropertiesProvider propertiesProvider) throws ConfigurationException { + try { + return OidcTokenCacheConfiguration.parse(propertiesProvider.getConfiguration("jmap")); + } catch (FileNotFoundException e) { + return OidcTokenCacheConfiguration.DEFAULT; + } + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
