diff --git a/docs/modules/ROOT/pages/tmail-backend/configure/rate-limiting.adoc b/docs/modules/ROOT/pages/tmail-backend/configure/rate-limiting.adoc index d1b479a02e..dab5812138 100644 --- a/docs/modules/ROOT/pages/tmail-backend/configure/rate-limiting.adoc +++ b/docs/modules/ROOT/pages/tmail-backend/configure/rate-limiting.adoc @@ -2,33 +2,3 @@ :navtitle: Rate limiting See xref:tmail-backend/features/tmailRateLimiting.adoc[this page] for feature explanation. - -== Apply rate limiting plans -An administrator needs to rely on the `EnforceRateLimitingPlan` mailet in order to apply rate limiting plans. - -Configure mailets to apply rate limiting to senders: - -.... - - some-prefix - TransitLimitations - tooMuchMails - - - some-prefix - RelayLimitations - tooMuchMails - -.... - -Configure mailet to apply rate limiting to recipients: - -.... - - some-prefix - DeliveryLimitations - tooMuchMails - -.... - -Exceeded emails will be transferred to `tooMuchMails` processor. Please customize this mailet depends on your will. diff --git a/docs/modules/ROOT/pages/tmail-backend/features/tmailRateLimiting.adoc b/docs/modules/ROOT/pages/tmail-backend/features/tmailRateLimiting.adoc index 964cb527df..30fdfdc06d 100644 --- a/docs/modules/ROOT/pages/tmail-backend/features/tmailRateLimiting.adoc +++ b/docs/modules/ROOT/pages/tmail-backend/features/tmailRateLimiting.adoc @@ -3,20 +3,14 @@ Manage email rate limiting plans and apply them to your users! -TMail Rate Limiting enable the administrator to manage rate limiting plans and apply those plans to users: +TMail Rate Limiting enable the administrator to manage rate limiting of users: -- Administrator can manage rate limiting plans -- Administrator can manage which rate limiting plans apply to which users -- Administrator can configure mailet configuration, so it can retrieve rate limiting plans and apply to according users +- Administrator can manage rate limiting for each user +- Administrator can configure mailet configuration, so it can retrieve rate limiting and apply to according users == Manage rate limiting plans via webadmin -The administrator can manage rate limiting plans using xref:tmail-backend/webadmin.adoc#_rate_limiting[Rate Limiting Plan Management Routes]: +The administrator can manage rate limiting using xref:tmail-backend/webadmin.adoc#_rate_limiting[Rate Limiting Plan Management Routes]: -- Creating/updating/getting rate limiting plans -- Attaching/getting/removing plan of a user. Getting list of users belonging to a plan. - -The administrator only can apply a plan to a user. - -Because of limited number of plans, we do not support removing rate limiting plans. +- Updating/getting rate limiting for users. diff --git a/docs/modules/ROOT/pages/tmail-backend/webadmin.adoc b/docs/modules/ROOT/pages/tmail-backend/webadmin.adoc index 1c28ed45c3..486a1b60d5 100644 --- a/docs/modules/ROOT/pages/tmail-backend/webadmin.adoc +++ b/docs/modules/ROOT/pages/tmail-backend/webadmin.adoc @@ -191,279 +191,6 @@ Return codes: - `400` Invalid request - `404` User does not exist -=== Create a new plan -Allow to create a new rate limiting plan. -.... -curl -XPOST http://ip:port/rate-limit-plans/{RateLimitingPlanName} - -H "Content-Type: application/json" - -d '{ - "transitLimits": [{ - "name": "receivedMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }, - { - "name": "receivedMailsPerDay", - "periodInSeconds": 86400, - "count": 1000, - "size": 4096 - } - ], - "relayLimits": [{ - "name": "relayMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }], - "deliveryLimits": [{ - "name": "deliveryMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }] -}' -.... - -Return planId of created plan: -.... -{ - "planId": "6b427706-11de-4674-a4e7-166983d9119e" -} -.... -Return codes: - -- `201` The plan created successfully -- `400` Invalid request - -=== Edit a plan - -.... -curl -XPUT http://ip:port/rate-limit-plans/{RateLimitingPlanId} - -H "Content-Type: application/json" - -d '{ - "transitLimits": [ - { - "name": "receivedMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }, - { - "name": "receivedMailsPerDay", - "periodInSeconds": 86400 - "count": 1000, - "size": 4096 - } - ], - "relayLimits": [ - { - "name": "relayMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - } - ], - "deliveryLimits": [ - { - "name": "deliveryMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - } - ] -}' -.... - -Allow to update an existing plan. - -Return codes: - -- `204` The plan updated successfully -- `400` Invalid request -- `404` Plan does not exist - -=== Get a plan - -.... -curl -XGET http://ip:port/rate-limit-plans/{RateLimitingPlanId} -.... - -Return a plan: -.... -{ - "planId": "65b94d87-b077-4994-bc82-ab87c4e68313", - "planName": "oldPlanName", - "transitLimits": [{ - "name": "receivedMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }, - { - "name": "receivedMailsPerDay", - "periodInSeconds": 86400, - "count": 1000, - "size": 4096 - } - ], - "relayLimits": [{ - "name": "relayMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }], - "deliveryLimits": [{ - "name": "deliveryMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }] -} -.... - -Return codes: - -- `200` Get the plan successfully -- `400` Invalid request -- `404` Plan does not exist - -=== Get all plans - -.... -curl -XGET http://ip:port/rate-limit-plans -.... - -Return all existing plans: -.... -[{ - "planId": "524acec6-7910-4137-b862-7ec1ab048404", - "planName": "plan1", - "transitLimits": [{ - "name": "receivedMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }, - { - "name": "receivedMailsPerDay", - "periodInSeconds": 86400, - "count": 1000, - "size": 4096 - } - ], - "relayLimits": [{ - "name": "relayMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }], - "deliveryLimits": [{ - "name": "deliveryMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }] - }, - { - "planId": "2fc6b2d7-9b62-42f0-aa8a-5ab62168e0c5", - "planName": "plan2", - "transitLimits": [{ - "name": "receivedMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }, - { - "name": "receivedMailsPerDay", - "periodInSeconds": 86400, - "count": 1000, - "size": 4096 - } - ], - "relayLimits": [{ - "name": "relayMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }], - "deliveryLimits": [{ - "name": "deliveryMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }] - } -] -.... - -Return codes: - -- `200` Get all plans successfully - -=== Attach a plan to a user -.... -curl -XPUT http://ip:port/users/{username}/rate-limit-plans/{planId} -.... - -Attach a rate limiting plan to a user. This also can be used to reattach a new plan to that user. - -Return codes: - -- `204` Attached the plan to the user successfully -- `400` Invalid request -- `404` Either plan or user is not found - -=== Get list of users belonging to a plan -.... -curl -XGET http://ip:port/rate-limit-plans/{planId}/users -.... - -Return users belong to a plan: -.... -[ - "bob@linagora.com", - "andre@linagora.com" -] -.... - -Return codes: - -- `200` Get all users belong to that plan successfully -- `400` Invalid request -- `404` Plan is not found - -=== Get plan of a user -.... -curl -XGET http://ip:port/users/{username}/rate-limit-plans -.... - -Return rate limiting planId attached to that user: -.... -{ - "planId": "02242f08-515c-4170-945e-64afa991f149" -} -.... - -Return codes: - -- `200` Get plan of that user successfully -- `400` Invalid request -- `404` Either user is not found or that user does not have a plan. - -=== Revoke plan of a user -.... -curl -XDELETE http://ip:port/users/{username}/rate-limit-plans -.... - -Revoke the plan attached to that user. - -Return codes: - -- `204` Revoke plan of that user successfully -- `400` Invalid request -- `404` User is not found - == Domain contacts === Create a contact diff --git a/tmail-backend/apps/distributed/src/main/java/com/linagora/tmail/james/app/DistributedServer.java b/tmail-backend/apps/distributed/src/main/java/com/linagora/tmail/james/app/DistributedServer.java index f2c3533400..5cc3a4eb08 100644 --- a/tmail-backend/apps/distributed/src/main/java/com/linagora/tmail/james/app/DistributedServer.java +++ b/tmail-backend/apps/distributed/src/main/java/com/linagora/tmail/james/app/DistributedServer.java @@ -211,7 +211,7 @@ import com.linagora.tmail.team.TeamMailboxModule; import com.linagora.tmail.webadmin.EmailAddressContactRoutesModule; import com.linagora.tmail.webadmin.OidcBackchannelLogoutRoutesModule; -import com.linagora.tmail.webadmin.RateLimitPlanRoutesModule; +import com.linagora.tmail.webadmin.RateLimitsRoutesModule; import com.linagora.tmail.webadmin.TeamMailboxRoutesModule; import com.linagora.tmail.webadmin.archival.InboxArchivalTaskModule; import com.linagora.tmail.webadmin.cleanup.MailboxesCleanupModule; @@ -257,7 +257,7 @@ protected void configure() { new MailboxRoutesModule(), new MailQueueRoutesModule(), new MailRepositoriesRoutesModule(), - new RateLimitPlanRoutesModule(), + new RateLimitsRoutesModule(), new TeamMailboxModule(), new TeamMailboxRoutesModule(), new SieveRoutesModule(), diff --git a/tmail-backend/apps/memory/src/main/java/com/linagora/tmail/james/app/MemoryServer.java b/tmail-backend/apps/memory/src/main/java/com/linagora/tmail/james/app/MemoryServer.java index 86a75b9ce7..ca94fc0af3 100644 --- a/tmail-backend/apps/memory/src/main/java/com/linagora/tmail/james/app/MemoryServer.java +++ b/tmail-backend/apps/memory/src/main/java/com/linagora/tmail/james/app/MemoryServer.java @@ -114,7 +114,7 @@ import com.linagora.tmail.team.TeamMailboxModule; import com.linagora.tmail.webadmin.EmailAddressContactRoutesModule; import com.linagora.tmail.webadmin.OidcBackchannelLogoutRoutesModule; -import com.linagora.tmail.webadmin.RateLimitPlanRoutesModule; +import com.linagora.tmail.webadmin.RateLimitsRoutesModule; import com.linagora.tmail.webadmin.TeamMailboxRoutesModule; import com.linagora.tmail.webadmin.archival.InboxArchivalTaskModule; import com.linagora.tmail.webadmin.cleanup.MailboxesCleanupModule; @@ -179,7 +179,7 @@ public class MemoryServer { new TMailScanningQuotaSearcherModule(), new MemoryRateLimiterModule(), new MemoryRateLimitingModule(), - new RateLimitPlanRoutesModule(), + new RateLimitsRoutesModule(), new MemoryEmailAddressContactModule(), new EmailAddressContactRoutesModule(), new MemoryLabelRepositoryModule(), diff --git a/tmail-backend/apps/postgres/src/main/java/com/linagora/tmail/james/app/PostgresTmailServer.java b/tmail-backend/apps/postgres/src/main/java/com/linagora/tmail/james/app/PostgresTmailServer.java index 3b6074a0ff..b9dbf94e87 100644 --- a/tmail-backend/apps/postgres/src/main/java/com/linagora/tmail/james/app/PostgresTmailServer.java +++ b/tmail-backend/apps/postgres/src/main/java/com/linagora/tmail/james/app/PostgresTmailServer.java @@ -187,7 +187,7 @@ import com.linagora.tmail.team.TeamMailboxModule; import com.linagora.tmail.webadmin.EmailAddressContactRoutesModule; import com.linagora.tmail.webadmin.OidcBackchannelLogoutRoutesModule; -import com.linagora.tmail.webadmin.RateLimitPlanRoutesModule; +import com.linagora.tmail.webadmin.RateLimitsRoutesModule; import com.linagora.tmail.webadmin.TeamMailboxRoutesModule; import com.linagora.tmail.webadmin.archival.InboxArchivalTaskModule; import com.linagora.tmail.webadmin.cleanup.MailboxesCleanupModule; @@ -278,7 +278,7 @@ public static GuiceJamesServer createServer(PostgresTmailConfiguration configura new MailQueueRoutesModule(), new MailRepositoriesRoutesModule(), new MessagesRoutesModule(), - new RateLimitPlanRoutesModule(), + new RateLimitsRoutesModule(), new ReIndexingModule(), new SieveRoutesModule(), new TeamMailboxModule(), @@ -356,7 +356,7 @@ public static GuiceJamesServer createServer(PostgresTmailConfiguration configura .with(new TeamMailboxModule(), new TMailMailboxSortOrderProviderModule(), new PostgresRateLimitingModule(), - new RateLimitPlanRoutesModule(), + new RateLimitsRoutesModule(), new EmailAddressContactRoutesModule(), new PostgresLabelRepositoryModule(), new PostgresJmapSettingsRepositoryModule(), diff --git a/tmail-backend/integration-tests/pom.xml b/tmail-backend/integration-tests/pom.xml index db50bacc16..01c5d3c978 100644 --- a/tmail-backend/integration-tests/pom.xml +++ b/tmail-backend/integration-tests/pom.xml @@ -48,10 +48,6 @@ webadmin/distributed-webadmin-integration-tests webadmin/postgres-webadmin-integration-tests - rate-limiter/rate-limiter-integration-tests-common - rate-limiter/distributed-rate-limiter-integration-tests - rate-limiter/postgres-rate-limiter-integration-tests - rspamd/rspamd-integration-tests-common rspamd/distributed-rspamd-integration-tests rspamd/postgres-rspamd-integration-tests @@ -89,11 +85,6 @@ webadmin-integration-tests-common ${project.version} - - ${project.groupId} - rate-limiter-integration-tests-common - ${project.version} - ${project.groupId} rspamd-integration-tests-common diff --git a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/pom.xml b/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/pom.xml deleted file mode 100644 index 5bfde42a29..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/pom.xml +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - com.linagora.tmail - integration-tests - 1.0.0-SNAPSHOT - ../../pom.xml - - 4.0.0 - distributed-rate-limiter-integration-tests - Twake Mail :: Integration Tests :: Rate Limiter :: Distributed - - - - ${project.groupId} - distributed - test - - - ${project.groupId} - rate-limiter-integration-tests-common - test - - - ${project.groupId} - distributed - test - test-jar - - - ${project.groupId} - smtp-extensions - - - ${project.groupId} - tmail-guice-distributed - test - test-jar - - - ${project.groupId} - tmail-guice-jmap - test - test-jar - - - ${project.groupId} - webadmin-integration-tests-common - test - - - ${james.groupId} - apache-james-backends-cassandra - test - test-jar - - - ${james.groupId} - apache-james-backends-opensearch - test - test-jar - - - ${james.groupId} - apache-james-backends-rabbitmq - test - test-jar - - - ${james.groupId} - apache-james-backends-redis - test - test-jar - - - ${james.groupId} - blob-s3 - test - test-jar - - - ${james.groupId} - blob-s3-guice - test - test-jar - - - ${james.groupId} - james-server-cassandra-app - test - test-jar - - - ${james.groupId} - james-server-distributed-app - test - test-jar - - - ${james.groupId} - james-server-guice-common - test - test-jar - - - ${james.groupId} - james-server-guice-jmap - test - test-jar - - - ${james.groupId} - james-server-rate-limiter-redis - test - test-jar - - - - \ No newline at end of file diff --git a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/java/com/linagora/tmail/integration/distributed/DistributedRateLimitingPlanIntegrationTest.java b/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/java/com/linagora/tmail/integration/distributed/DistributedRateLimitingPlanIntegrationTest.java deleted file mode 100644 index bd678a70a9..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/java/com/linagora/tmail/integration/distributed/DistributedRateLimitingPlanIntegrationTest.java +++ /dev/null @@ -1,70 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.integration.distributed; - -import org.apache.james.JamesServerBuilder; -import org.apache.james.JamesServerExtension; -import org.apache.james.backends.redis.RedisExtension; -import org.apache.james.mailrepository.api.MailRepositoryUrl; -import org.apache.james.modules.AwsS3BlobStoreExtension; -import org.apache.james.rate.limiter.redis.RedisRateLimiterModule; -import org.junit.jupiter.api.extension.RegisterExtension; - -import com.linagora.tmail.blob.guice.BlobStoreConfiguration; -import com.linagora.tmail.integration.RateLimitingPlanIntegrationContract; -import com.linagora.tmail.james.app.CassandraExtension; -import com.linagora.tmail.james.app.DistributedJamesConfiguration; -import com.linagora.tmail.james.app.DistributedServer; -import com.linagora.tmail.james.app.DockerOpenSearchExtension; -import com.linagora.tmail.james.app.EventBusKeysChoice; -import com.linagora.tmail.james.app.RabbitMQExtension; -import com.linagora.tmail.module.LinagoraTestJMAPServerModule; - -public class DistributedRateLimitingPlanIntegrationTest implements RateLimitingPlanIntegrationContract { - private static final MailRepositoryUrl ERROR_REPOSITORY = MailRepositoryUrl.from("cassandra://var/mail/error/"); - - @Override - public MailRepositoryUrl getErrorRepository() { - return ERROR_REPOSITORY; - } - - @RegisterExtension - static JamesServerExtension testExtension = new JamesServerBuilder(tmpDir -> - DistributedJamesConfiguration.builder() - .workingDirectory(tmpDir) - .configurationFromClasspath() - .blobStore(BlobStoreConfiguration.builder() - .s3() - .noSecondaryS3BlobStore() - .disableCache() - .deduplication() - .noCryptoConfig() - .disableSingleSave()) - .eventBusKeysChoice(EventBusKeysChoice.REDIS) - .build()) - .extension(new DockerOpenSearchExtension()) - .extension(new CassandraExtension()) - .extension(new RabbitMQExtension()) - .extension(new AwsS3BlobStoreExtension()) - .extension(new RedisExtension()) - .server(configuration -> DistributedServer.createServer(configuration) - .overrideWith(new RedisRateLimiterModule()) - .overrideWith(new LinagoraTestJMAPServerModule())) - .build(); -} diff --git a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/dnsservice.xml b/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/dnsservice.xml deleted file mode 100644 index 6e4fbd2efb..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/dnsservice.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - true - false - 50000 - diff --git a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/domainlist.xml b/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/domainlist.xml deleted file mode 100644 index fe17431a1e..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/domainlist.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - false - false - diff --git a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/fakemailrepositorystore.xml b/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/fakemailrepositorystore.xml deleted file mode 100644 index 2d19a802da..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/fakemailrepositorystore.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - file - - - - - diff --git a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/imapserver.xml b/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/imapserver.xml deleted file mode 100644 index 3434dbce39..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/imapserver.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - imapserver - 0.0.0.0:0 - 200 - - - classpath://keystore - james72laBalle - org.bouncycastle.jce.provider.BouncyCastleProvider - - 0 - 0 - false - false - - - imapserver-ssl - 0.0.0.0:0 - 200 - - - classpath://keystore - james72laBalle - org.bouncycastle.jce.provider.BouncyCastleProvider - - 0 - 0 - false - - diff --git a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/keystore b/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/keystore deleted file mode 100644 index 536a6c792b..0000000000 Binary files a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/keystore and /dev/null differ diff --git a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/listeners.xml b/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/listeners.xml deleted file mode 100644 index 003e852851..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/listeners.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - org.apache.james.mailbox.cassandra.MailboxOperationLoggingListener - - - org.apache.james.jmap.event.PopulateEmailQueryViewListener - true - - \ No newline at end of file diff --git a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/mailetcontainer.xml b/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/mailetcontainer.xml deleted file mode 100644 index b347e4d72c..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/mailetcontainer.xml +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - postmaster@localhost - - - 5 - - - - - transport - - - - - TransitLimitations - - - X-WasSigned - true - - - bcc - - - rrt-error - - - - - - - local-delivery - - - outgoing - 5000, 100000, 500000 - 3 - 0 - 10 - true - bounces - - - error - - - - - DeliveryLimitations - - - - - - - false - - - - - - cassandra://var/mail/error - - - - - - cassandra://var/mail/rrt-error/ - - - - - - \ No newline at end of file diff --git a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/mailrepositorystore.xml b/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/mailrepositorystore.xml deleted file mode 100644 index 626e0a2bad..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/mailrepositorystore.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - cassandra - - - - diff --git a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/pop3server.xml b/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/pop3server.xml deleted file mode 100644 index 6defc24721..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/pop3server.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - diff --git a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/rabbitmq.properties b/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/rabbitmq.properties deleted file mode 100644 index 0db6f33686..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/rabbitmq.properties +++ /dev/null @@ -1,20 +0,0 @@ -###################################################################### -# As a subpart of Twake Mail, this file is edited by Linagora. # -# # -# https://twake-mail.com/ # -# https://linagora.com # -# # -# This file is subject to The Affero Gnu Public License # -# version 3. # -# # -# https://www.gnu.org/licenses/agpl-3.0.en.html # -# # -# This program is distributed in the hope that it will be # -# useful, but WITHOUT ANY WARRANTY; without even the implied # -# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR # -# PURPOSE. See the GNU Affero General Public License for # -# more details. # -###################################################################### - -uri=amqp://james:james@rabbitmq_host:5672 -management.uri=http://james:james@rabbitmq_host:15672/api/ \ No newline at end of file diff --git a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/smtpserver.xml b/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/smtpserver.xml deleted file mode 100644 index 78ba994d15..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/smtpserver.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - smtpserver-global - 0.0.0.0:0 - 200 - - file://conf/keystore - james72laBalle - org.bouncycastle.jce.provider.BouncyCastleProvider - SunX509 - - 360 - 0 - 0 - false - false - 0 - true - Apache JAMES awesome SMTP Server - - - - - false - - - smtpserver-TLS - 0.0.0.0:0 - 200 - - file://conf/keystore - james72laBalle - org.bouncycastle.jce.provider.BouncyCastleProvider - SunX509 - - 360 - 0 - 0 - - true - - false - 0 - true - Apache JAMES awesome SMTP Server - - - - - false - - - smtpserver-authenticated - 0.0.0.0:0 - 200 - - file://conf/keystore - james72laBalle - org.bouncycastle.jce.provider.BouncyCastleProvider - SunX509 - - 360 - 0 - 0 - - true - - false - 0 - true - Apache JAMES awesome SMTP Server - - - - - false - - - - diff --git a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/webadmin.properties b/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/webadmin.properties deleted file mode 100644 index 78a176aabd..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/distributed-rate-limiter-integration-tests/src/test/resources/webadmin.properties +++ /dev/null @@ -1,27 +0,0 @@ -# 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. - -# This template file can be used as example for James Server configuration -# DO NOT USE IT AS SUCH AND ADAPT IT TO YOUR NEEDS - -# Read https://james.apache.org/server/config-webadmin.html for further details - -enabled=true -port=0 -host=127.0.0.1 - -extensions.routes=org.apache.james.webadmin.dropwizard.MetricsRoutes \ No newline at end of file diff --git a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/pom.xml b/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/pom.xml deleted file mode 100644 index a8472a21be..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/pom.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - com.linagora.tmail - integration-tests - 1.0.0-SNAPSHOT - ../../pom.xml - - 4.0.0 - postgres-rate-limiter-integration-tests - Twake Mail :: Integration Tests :: Rate Limiter :: Postgres - - - - ${project.groupId} - blobid-list - test - - - ${project.groupId} - postgres - test - - - ${project.groupId} - postgres - test-jar - test - - - ${project.groupId} - tmail-guice-distributed - test-jar - test - - - ${project.groupId} - tmail-guice-jmap - test - test-jar - - - ${project.groupId} - rate-limiter-integration-tests-common - test - - - ${james.groupId} - apache-james-backends-postgres - test-jar - test - - - ${james.groupId} - apache-james-backends-rabbitmq - test - test-jar - - - ${james.groupId} - apache-james-backends-redis - test - test-jar - - - org.testcontainers - postgresql - test - - - diff --git a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/java/com/linagora/tmail/integration/postgres/PostgresRateLimitingPlanIntegrationTest.java b/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/java/com/linagora/tmail/integration/postgres/PostgresRateLimitingPlanIntegrationTest.java deleted file mode 100644 index 67a1db2ed9..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/java/com/linagora/tmail/integration/postgres/PostgresRateLimitingPlanIntegrationTest.java +++ /dev/null @@ -1,67 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.integration.postgres; - -import org.apache.james.JamesServerBuilder; -import org.apache.james.JamesServerExtension; -import org.apache.james.PostgresJamesConfiguration; -import org.apache.james.SearchConfiguration; -import org.apache.james.backends.postgres.PostgresExtension; -import org.apache.james.backends.redis.RedisExtension; -import org.apache.james.mailrepository.api.MailRepositoryUrl; -import org.apache.james.rate.limiter.redis.RedisRateLimiterModule; -import org.junit.jupiter.api.extension.RegisterExtension; - -import com.linagora.tmail.blob.guice.BlobStoreConfiguration; -import com.linagora.tmail.integration.RateLimitingPlanIntegrationContract; -import com.linagora.tmail.james.app.PostgresTmailConfiguration; -import com.linagora.tmail.james.app.PostgresTmailServer; -import com.linagora.tmail.james.app.RabbitMQExtension; -import com.linagora.tmail.module.LinagoraTestJMAPServerModule; - -public class PostgresRateLimitingPlanIntegrationTest implements RateLimitingPlanIntegrationContract { - private static final MailRepositoryUrl ERROR_REPOSITORY = MailRepositoryUrl.from("postgres://var/mail/error/"); - - @Override - public MailRepositoryUrl getErrorRepository() { - return ERROR_REPOSITORY; - } - - @RegisterExtension - static JamesServerExtension testExtension = new JamesServerBuilder(tmpDir -> - PostgresTmailConfiguration.builder() - .workingDirectory(tmpDir) - .configurationFromClasspath() - .searchConfiguration(SearchConfiguration.scanning()) - .eventBusImpl(PostgresJamesConfiguration.EventBusImpl.RABBITMQ) - .blobStore(BlobStoreConfiguration.builder() - .postgres() - .disableCache() - .deduplication() - .noCryptoConfig() - .disableSingleSave()) - .build()) - .extension(PostgresExtension.empty()) - .extension(new RabbitMQExtension()) - .extension(new RedisExtension()) - .server(configuration -> PostgresTmailServer.createServer(configuration) - .overrideWith(new LinagoraTestJMAPServerModule()) - .overrideWith(new RedisRateLimiterModule())) - .build(); -} diff --git a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/dnsservice.xml b/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/dnsservice.xml deleted file mode 100644 index 6e4fbd2efb..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/dnsservice.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - true - false - 50000 - diff --git a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/domainlist.xml b/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/domainlist.xml deleted file mode 100644 index fe17431a1e..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/domainlist.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - false - false - diff --git a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/fakemailrepositorystore.xml b/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/fakemailrepositorystore.xml deleted file mode 100644 index 2d19a802da..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/fakemailrepositorystore.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - file - - - - - diff --git a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/imapserver.xml b/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/imapserver.xml deleted file mode 100644 index 3434dbce39..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/imapserver.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - imapserver - 0.0.0.0:0 - 200 - - - classpath://keystore - james72laBalle - org.bouncycastle.jce.provider.BouncyCastleProvider - - 0 - 0 - false - false - - - imapserver-ssl - 0.0.0.0:0 - 200 - - - classpath://keystore - james72laBalle - org.bouncycastle.jce.provider.BouncyCastleProvider - - 0 - 0 - false - - diff --git a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/keystore b/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/keystore deleted file mode 100644 index 536a6c792b..0000000000 Binary files a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/keystore and /dev/null differ diff --git a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/listeners.xml b/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/listeners.xml deleted file mode 100644 index 74e79d927f..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/listeners.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - org.apache.james.jmap.event.PopulateEmailQueryViewListener - true - - \ No newline at end of file diff --git a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/mailetcontainer.xml b/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/mailetcontainer.xml deleted file mode 100644 index e06ea9b4d8..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/mailetcontainer.xml +++ /dev/null @@ -1,76 +0,0 @@ - - - - postmaster@localhost - - - 5 - - - - - transport - - - - - TransitLimitations - - - X-WasSigned - true - - - bcc - - - rrt-error - - - - - - - local-delivery - - - outgoing - 5000, 100000, 500000 - 3 - 0 - 10 - true - bounces - - - error - - - - - DeliveryLimitations - - - - - - - false - - - - - - postgres://var/mail/error - - - - - - postgres://var/mail/rrt-error/ - - - - - - \ No newline at end of file diff --git a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/mailrepositorystore.xml b/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/mailrepositorystore.xml deleted file mode 100644 index 573ec24ad3..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/mailrepositorystore.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - postgres - - - - diff --git a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/managesieveserver.xml b/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/managesieveserver.xml deleted file mode 100644 index f136a432b8..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/managesieveserver.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - diff --git a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/pop3server.xml b/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/pop3server.xml deleted file mode 100644 index 2a62f6f789..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/pop3server.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - diff --git a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/rabbitmq.properties b/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/rabbitmq.properties deleted file mode 100644 index 25d0dd6a97..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/rabbitmq.properties +++ /dev/null @@ -1,2 +0,0 @@ -uri=amqp://james:james@rabbitmq_host:5672 -management.uri=http://james:james@rabbitmq_host:15672/api/ \ No newline at end of file diff --git a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/smtpserver.xml b/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/smtpserver.xml deleted file mode 100644 index 07178db100..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/smtpserver.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - smtpserver-global - 0.0.0.0:0 - 200 - - file://conf/keystore - james72laBalle - org.bouncycastle.jce.provider.BouncyCastleProvider - SunX509 - - 360 - 0 - 0 - false - - never - false - true - - 0 - true - Apache JAMES awesome SMTP Server - - - - - false - - \ No newline at end of file diff --git a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/usersrepository.xml b/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/usersrepository.xml deleted file mode 100644 index f8c8a25872..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/usersrepository.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - true - SHA-1 - diff --git a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/webadmin.properties b/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/webadmin.properties deleted file mode 100644 index 3386a14238..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/postgres-rate-limiter-integration-tests/src/test/resources/webadmin.properties +++ /dev/null @@ -1,25 +0,0 @@ -# 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. - -# This template file can be used as example for James Server configuration -# DO NOT USE IT AS SUCH AND ADAPT IT TO YOUR NEEDS - -# Read https://james.apache.org/server/config-webadmin.html for further details - -enabled=true -port=0 -host=127.0.0.1 \ No newline at end of file diff --git a/tmail-backend/integration-tests/rate-limiter/rate-limiter-integration-tests-common/pom.xml b/tmail-backend/integration-tests/rate-limiter/rate-limiter-integration-tests-common/pom.xml deleted file mode 100644 index 1d5e59a3d8..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/rate-limiter-integration-tests-common/pom.xml +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - integration-tests - com.linagora.tmail - 1.0.0-SNAPSHOT - ../../pom.xml - - 4.0.0 - - rate-limiter-integration-tests-common - Twake Mail :: Integration Tests :: Rate Limiter :: Common - - - - ${james.groupId} - james-server-guice-common - - - ${james.groupId} - james-server-guice-imap - - - ${james.groupId} - james-server-guice-mailbox - - - ${james.groupId} - james-server-guice-smtp - - - ${james.groupId} - james-server-guice-webadmin - - - ${james.groupId} - james-server-mailets-integration-testing - - - ${james.groupId} - james-server-testing - - - ${james.groupId} - james-server-webadmin-core - test-jar - - - - - - - net.alchim31.maven - scala-maven-plugin - - - io.github.evis - scalafix-maven-plugin_2.13 - - ${project.parent.parent.parent.basedir}/.scalafix.conf - - - - - - \ No newline at end of file diff --git a/tmail-backend/integration-tests/rate-limiter/rate-limiter-integration-tests-common/src/main/scala/com/linagora/tmail/integration/RateLimitingPlanIntegrationContract.scala b/tmail-backend/integration-tests/rate-limiter/rate-limiter-integration-tests-common/src/main/scala/com/linagora/tmail/integration/RateLimitingPlanIntegrationContract.scala deleted file mode 100644 index f36a00fe50..0000000000 --- a/tmail-backend/integration-tests/rate-limiter/rate-limiter-integration-tests-common/src/main/scala/com/linagora/tmail/integration/RateLimitingPlanIntegrationContract.scala +++ /dev/null @@ -1,270 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.integration - -import java.util.stream.IntStream - -import com.linagora.tmail.integration.RateLimitingPlanIntegrationContract.{DOMAIN, RECIPIENT1, RECIPIENT2, RECIPIENT3, SENDER1, SENDER2} -import io.restassured.RestAssured -import io.restassured.RestAssured.{`given`, requestSpecification} -import io.restassured.http.ContentType.JSON -import org.apache.http.HttpStatus.{SC_CREATED, SC_NO_CONTENT} -import org.apache.james.GuiceJamesServer -import org.apache.james.core.{Domain, Username} -import org.apache.james.mailets.configuration.Constants.{LOCALHOST_IP, PASSWORD, awaitAtMostOneMinute} -import org.apache.james.mailrepository.api.MailRepositoryUrl -import org.apache.james.modules.protocols.{ImapGuiceProbe, SmtpGuiceProbe} -import org.apache.james.utils.TestIMAPClient.INBOX -import org.apache.james.utils.{DataProbeImpl, MailRepositoryProbeImpl, SMTPMessageSender, TestIMAPClient, WebAdminGuiceProbe} -import org.apache.james.webadmin.WebAdminUtils -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.{BeforeEach, Test} - -object RateLimitingPlanIntegrationContract { - val DOMAIN: Domain = Domain.of("domain.tld") - val SENDER1: Username = Username.fromLocalPartWithDomain("sender1", DOMAIN) - val SENDER2: Username = Username.fromLocalPartWithDomain("sender2", DOMAIN) - val RECIPIENT1: Username = Username.fromLocalPartWithDomain("recipient1", DOMAIN) - val RECIPIENT2: Username = Username.fromLocalPartWithDomain("recipient2", DOMAIN) - val RECIPIENT3: Username = Username.fromLocalPartWithDomain("recipient3", DOMAIN) -} - -trait RateLimitingPlanIntegrationContract { - - def getErrorRepository: MailRepositoryUrl - - @BeforeEach - def setUp(server: GuiceJamesServer): Unit = { - server.getProbe(classOf[DataProbeImpl]) - .fluent() - .addDomain(DOMAIN.asString()) - .addUser(SENDER1.asString(), PASSWORD) - .addUser(SENDER2.asString(), PASSWORD) - .addUser(RECIPIENT1.asString(), PASSWORD) - .addUser(RECIPIENT2.asString(), PASSWORD) - .addUser(RECIPIENT3.asString(), PASSWORD) - - requestSpecification = WebAdminUtils.buildRequestSpecification(server.getProbe(classOf[WebAdminGuiceProbe]).getWebAdminPort) - .build - RestAssured.enableLoggingOfRequestAndResponseIfValidationFails() - } - - private def messageSender(server: GuiceJamesServer): SMTPMessageSender = - new SMTPMessageSender(DOMAIN.asString()) - .connect(LOCALHOST_IP, server.getProbe(classOf[SmtpGuiceProbe]).getSmtpPort) - - private def testImapClient(server: GuiceJamesServer): TestIMAPClient = - new TestIMAPClient().connect(LOCALHOST_IP, server.getProbe(classOf[ImapGuiceProbe]).getImapPort) - - private def createNewPlan(): String = { - val json: String = - """{ - | "transitLimits": [ - | { - | "name": "receivedMailsPerHour", - | "periodInSeconds": 3600, - | "count": 1, - | "size": 2048 - | } - | ], - | "relayLimits": [ - | { - | "name": "relayMailsPerHour", - | "periodInSeconds": 3600, - | "count": 1, - | "size": 2048 - | } - | ], - | "deliveryLimits": [ - | { - | "name": "deliveryMailsPerHour", - | "periodInSeconds": 3600, - | "count": 1, - | "size": 2048 - | } - | ] - |}""".stripMargin - - createNewPlan(json, "planTest") - } - - private def createNewPlan(limitJson: String, planName: String): String = - given().basePath("/rate-limit-plans/" + planName) - .body(limitJson) - .when() - .post() - .`then`() - .statusCode(SC_CREATED) - .contentType(JSON) - .extract() - .jsonPath() - .get("planId").asInstanceOf[String] - - - private def attachPlanToUser(planId: String, username: Username): Unit = - given().basePath(String.format("/users/%s/rate-limit-plans/%s", username.asString(), planId)) - .put() - .`then`() - .statusCode(SC_NO_CONTENT) - - @Test - def senderShouldSentEmailWhenRateLimitPlanIsAcceptable(server: GuiceJamesServer): Unit = { - attachPlanToUser(createNewPlan(), SENDER1) - - messageSender(server).authenticate(SENDER1.asString(), PASSWORD) - .sendMessageWithHeaders(SENDER1.asString, RECIPIENT1.asString, "From: " + SENDER1.asString() + "\r\n\r\nsubject: test123\r\rcontent 123\r.\r") - - val imapClient: TestIMAPClient = testImapClient(server) - imapClient.login(RECIPIENT1.asString(), PASSWORD) - .select(INBOX) - .awaitMessage(awaitAtMostOneMinute) - - assertThat(imapClient.readFirstMessage).contains("content 123") - } - - @Test - def senderShouldNotSentEmailWhenRateLimitPlanIsExceeded(server: GuiceJamesServer): Unit = { - attachPlanToUser(createNewPlan(), SENDER1) - - // accept - messageSender(server).authenticate(SENDER1.asString(), PASSWORD) - .sendMessageWithHeaders(SENDER1.asString, RECIPIENT1.asString, "From: " + SENDER1.asString() + "\r\n\r\nsubject: test1\r\rcontent 1\r.\r") - - testImapClient(server).login(RECIPIENT1.asString(), PASSWORD) - .select(INBOX) - .awaitMessage(awaitAtMostOneMinute) - - // exceed - messageSender(server).authenticate(SENDER1.asString(), PASSWORD) - .sendMessageWithHeaders(SENDER1.asString, RECIPIENT2.asString, "From: " + SENDER1.asString() + "\r\n\r\nsubject: test2\r\rcontent 2\r.\r") - - awaitAtMostOneMinute.until(() => server.getProbe(classOf[MailRepositoryProbeImpl]) - .getRepositoryMailCount(getErrorRepository) == 1) - - assertThat(testImapClient(server) - .login(RECIPIENT2, PASSWORD) - .getMessageCount(INBOX)) - .isEqualTo(0) - } - - @Test - def recipientShouldReceivedEmailWhenRateLimitPlanIsAcceptable(server: GuiceJamesServer): Unit = { - attachPlanToUser(createNewPlan(), RECIPIENT1) - - messageSender(server).authenticate(SENDER1.asString(), PASSWORD) - .sendMessageWithHeaders(SENDER1.asString, RECIPIENT1.asString, "From: " + SENDER1.asString() + "\r\n\r\nsubject: test123\r\rcontent 123\r.\r") - - val imapClient: TestIMAPClient = testImapClient(server) - imapClient.login(RECIPIENT1.asString(), PASSWORD) - .select(INBOX) - .awaitMessage(awaitAtMostOneMinute) - - assertThat(imapClient.readFirstMessage).contains("content 123") - } - - @Test - def recipientShouldNotReceivedEmailWhenRateLimitPlanIsExceeded(server: GuiceJamesServer): Unit = { - attachPlanToUser(createNewPlan(), RECIPIENT1) - // accept - messageSender(server).authenticate(SENDER1.asString(), PASSWORD) - .sendMessageWithHeaders(SENDER1.asString, RECIPIENT1.asString, "From: " + SENDER1.asString() + "\r\n\r\nsubject: test1\r\rcontent 1\r.\r") - - testImapClient(server).login(RECIPIENT1.asString(), PASSWORD) - .select(INBOX) - .awaitMessage(awaitAtMostOneMinute) - - // exceed - messageSender(server).authenticate(SENDER1.asString(), PASSWORD) - .sendMessageWithHeaders(SENDER1.asString, RECIPIENT1.asString, "From: " + SENDER1.asString() + "\r\n\r\nsubject: test2\r\rcontent 2\r.\r") - - awaitAtMostOneMinute.until(() => server.getProbe(classOf[MailRepositoryProbeImpl]) - .getRepositoryMailCount(getErrorRepository) == 1) - - assertThat(testImapClient(server) - .login(RECIPIENT1, PASSWORD) - .getMessageCount(INBOX)) - .isEqualTo(1) - } - - @Test - def differentPlansApplyToDifferentUsersShouldRateLimitPerSender(server: GuiceJamesServer): Unit = { - val planA: String = createNewPlan( - """{ - | "transitLimits": [ - | { - | "name": "receivedMailsPerHour", - | "periodInSeconds": 3600, - | "count": 1, - | "size": 2048 - | } - | ] - |}""".stripMargin, "planA") - - val countLimitPlanB: Int = 5 - val planB: String = createNewPlan( - s"""{ - | "transitLimits": [ - | { - | "name": "receivedMailsPerHour", - | "periodInSeconds": 3600, - | "count": $countLimitPlanB, - | "size": 2000048 - | } - | ] - |}""".stripMargin, "planB") - - attachPlanToUser(planA, SENDER1) - attachPlanToUser(planB, SENDER2) - - // Checking sender1 with planA - messageSender(server).authenticate(SENDER1.asString(), PASSWORD) - .sendMessageWithHeaders(SENDER1.asString, RECIPIENT1.asString, "From: " + SENDER1.asString() + "\r\n\r\nsubject: test1\r\rcontent 1\r.\r") - - testImapClient(server).login(RECIPIENT1.asString(), PASSWORD) - .select(INBOX) - .awaitMessage(awaitAtMostOneMinute) - - messageSender(server).authenticate(SENDER1.asString(), PASSWORD) - .sendMessageWithHeaders(SENDER1.asString, RECIPIENT2.asString, "From: " + SENDER1.asString() + "\r\n\r\nsubject: test2\r\rcontent 2\r.\r") - - awaitAtMostOneMinute.until(() => server.getProbe(classOf[MailRepositoryProbeImpl]) - .getRepositoryMailCount(getErrorRepository) == 1) - - assertThat(testImapClient(server) - .login(RECIPIENT2, PASSWORD) - .getMessageCount(INBOX)) - .isEqualTo(0) - - // Checking sender2 with planB - IntStream.range(0, countLimitPlanB) - .forEach(index => - messageSender(server).authenticate(SENDER2.asString(), PASSWORD) - .sendMessageWithHeaders(SENDER2.asString, RECIPIENT3.asString, s"""From: ${SENDER2.asString}\r\n\r\nsubject: test3+$index\r\rcontent 3\r.\r""")) - - awaitAtMostOneMinute.until(() => testImapClient(server) - .login(RECIPIENT3, PASSWORD) - .getMessageCount(INBOX) == countLimitPlanB) - - messageSender(server).authenticate(SENDER2.asString(), PASSWORD) - .sendMessageWithHeaders(SENDER2.asString, RECIPIENT3.asString, s"""From: ${SENDER2.asString}\r\n\r\nsubject: test4\r\rcontent 4\r.\r""") - awaitAtMostOneMinute.until(() => server.getProbe(classOf[MailRepositoryProbeImpl]) - .getRepositoryMailCount(getErrorRepository) == 2) - } - -} diff --git a/tmail-backend/integration-tests/webadmin/distributed-webadmin-integration-tests/src/test/java/com/linagora/tmail/integration/distributed/DistributedUsernameChangeIntegrationTest.java b/tmail-backend/integration-tests/webadmin/distributed-webadmin-integration-tests/src/test/java/com/linagora/tmail/integration/distributed/DistributedUsernameChangeIntegrationTest.java index d17d112e08..cc6b28119b 100644 --- a/tmail-backend/integration-tests/webadmin/distributed-webadmin-integration-tests/src/test/java/com/linagora/tmail/integration/distributed/DistributedUsernameChangeIntegrationTest.java +++ b/tmail-backend/integration-tests/webadmin/distributed-webadmin-integration-tests/src/test/java/com/linagora/tmail/integration/distributed/DistributedUsernameChangeIntegrationTest.java @@ -41,7 +41,6 @@ import com.google.inject.multibindings.Multibinder; import com.linagora.tmail.blob.guice.BlobStoreConfiguration; import com.linagora.tmail.integration.UsernameChangeIntegrationContract; -import com.linagora.tmail.integration.probe.RateLimitingProbe; import com.linagora.tmail.james.app.CassandraExtension; import com.linagora.tmail.james.app.DistributedEncryptedMailboxModule; import com.linagora.tmail.james.app.DistributedJamesConfiguration; @@ -94,9 +93,6 @@ public class DistributedUsernameChangeIntegrationTest extends UsernameChangeInte .overrideWith(binder -> Multibinder.newSetBinder(binder, GuiceProbe.class) .addBinding() .to(JmapGuiceKeystoreManagerProbe.class)) - .overrideWith(binder -> Multibinder.newSetBinder(binder, GuiceProbe.class) - .addBinding() - .to(RateLimitingProbe.class)) .overrideWith(binder -> Multibinder.newSetBinder(binder, GuiceProbe.class) .addBinding() .to(JmapGuiceLabelProbe.class)) diff --git a/tmail-backend/integration-tests/webadmin/postgres-webadmin-integration-tests/src/test/java/com/linagora/tmail/integration/postgres/PostgresUsernameChangeIntegrationTest.java b/tmail-backend/integration-tests/webadmin/postgres-webadmin-integration-tests/src/test/java/com/linagora/tmail/integration/postgres/PostgresUsernameChangeIntegrationTest.java index b1d4967214..891fa8c84b 100644 --- a/tmail-backend/integration-tests/webadmin/postgres-webadmin-integration-tests/src/test/java/com/linagora/tmail/integration/postgres/PostgresUsernameChangeIntegrationTest.java +++ b/tmail-backend/integration-tests/webadmin/postgres-webadmin-integration-tests/src/test/java/com/linagora/tmail/integration/postgres/PostgresUsernameChangeIntegrationTest.java @@ -45,7 +45,6 @@ import com.linagora.tmail.combined.identity.UsersRepositoryClassProbe; import com.linagora.tmail.encrypted.MailboxManagerClassProbe; import com.linagora.tmail.integration.UsernameChangeIntegrationContract; -import com.linagora.tmail.integration.probe.RateLimitingProbe; import com.linagora.tmail.james.app.DockerOpenSearchExtension; import com.linagora.tmail.james.app.PostgresEncryptedMailboxModule; import com.linagora.tmail.james.app.PostgresTmailConfiguration; @@ -89,7 +88,6 @@ public class PostgresUsernameChangeIntegrationTest extends UsernameChangeIntegra .overrideWith(binder -> Multibinder.newSetBinder(binder, GuiceProbe.class).addBinding().to(JmapGuiceContactAutocompleteProbe.class)) .overrideWith(binder -> Multibinder.newSetBinder(binder, GuiceProbe.class).addBinding().to(JmapSettingsProbe.class)) .overrideWith(binder -> Multibinder.newSetBinder(binder, GuiceProbe.class).addBinding().to(JmapGuiceLabelProbe.class)) - .overrideWith(binder -> Multibinder.newSetBinder(binder, GuiceProbe.class).addBinding().to(RateLimitingProbe.class)) .overrideWith(binder -> Multibinder.newSetBinder(binder, GuiceProbe.class).addBinding().to(JmapGuiceKeystoreManagerProbe.class))) .extension(PostgresExtension.empty()) .extension(new ClockExtension()) diff --git a/tmail-backend/integration-tests/webadmin/postgres-webadmin-integration-tests/src/test/java/com/linagora/tmail/integration/postgres/PostgresWebAdminBase.java b/tmail-backend/integration-tests/webadmin/postgres-webadmin-integration-tests/src/test/java/com/linagora/tmail/integration/postgres/PostgresWebAdminBase.java index 64de2a7e60..f2b027fb45 100644 --- a/tmail-backend/integration-tests/webadmin/postgres-webadmin-integration-tests/src/test/java/com/linagora/tmail/integration/postgres/PostgresWebAdminBase.java +++ b/tmail-backend/integration-tests/webadmin/postgres-webadmin-integration-tests/src/test/java/com/linagora/tmail/integration/postgres/PostgresWebAdminBase.java @@ -35,7 +35,6 @@ import com.linagora.tmail.blob.guice.BlobStoreConfiguration; import com.linagora.tmail.combined.identity.UsersRepositoryClassProbe; import com.linagora.tmail.encrypted.MailboxManagerClassProbe; -import com.linagora.tmail.integration.probe.RateLimitingProbe; import com.linagora.tmail.james.app.PostgresTmailConfiguration; import com.linagora.tmail.james.app.PostgresTmailServer; import com.linagora.tmail.james.common.probe.JmapGuiceContactAutocompleteProbe; @@ -67,7 +66,6 @@ public class PostgresWebAdminBase { .overrideWith(binder -> Multibinder.newSetBinder(binder, GuiceProbe.class).addBinding().to(JmapGuiceContactAutocompleteProbe.class)) .overrideWith(binder -> Multibinder.newSetBinder(binder, GuiceProbe.class).addBinding().to(JmapSettingsProbe.class)) .overrideWith(binder -> Multibinder.newSetBinder(binder, GuiceProbe.class).addBinding().to(JmapGuiceLabelProbe.class)) - .overrideWith(binder -> Multibinder.newSetBinder(binder, GuiceProbe.class).addBinding().to(RateLimitingProbe.class)) .overrideWith(overrideWithModule)) .extension(PostgresExtension.empty()) .extension(new ClockExtension()); diff --git a/tmail-backend/integration-tests/webadmin/webadmin-integration-tests-common/pom.xml b/tmail-backend/integration-tests/webadmin/webadmin-integration-tests-common/pom.xml index da43e83df5..1605fbf976 100644 --- a/tmail-backend/integration-tests/webadmin/webadmin-integration-tests-common/pom.xml +++ b/tmail-backend/integration-tests/webadmin/webadmin-integration-tests-common/pom.xml @@ -38,15 +38,6 @@ ${project.groupId} team-mailboxes - - ${project.groupId} - tmail-rate-limiter-api - - - ${project.groupId} - tmail-rate-limiter-api - test-jar - ${james.groupId} james-server-guice-common diff --git a/tmail-backend/integration-tests/webadmin/webadmin-integration-tests-common/src/main/java/com/linagora/tmail/integration/UsernameChangeIntegrationContract.java b/tmail-backend/integration-tests/webadmin/webadmin-integration-tests-common/src/main/java/com/linagora/tmail/integration/UsernameChangeIntegrationContract.java index ba857b2ce4..74cf2b1b21 100644 --- a/tmail-backend/integration-tests/webadmin/webadmin-integration-tests-common/src/main/java/com/linagora/tmail/integration/UsernameChangeIntegrationContract.java +++ b/tmail-backend/integration-tests/webadmin/webadmin-integration-tests-common/src/main/java/com/linagora/tmail/integration/UsernameChangeIntegrationContract.java @@ -42,7 +42,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import com.linagora.tmail.integration.probe.RateLimitingProbe; import com.linagora.tmail.james.common.probe.JmapGuiceContactAutocompleteProbe; import com.linagora.tmail.james.common.probe.JmapGuiceKeystoreManagerProbe; import com.linagora.tmail.james.common.probe.JmapGuiceLabelProbe; @@ -52,8 +51,6 @@ import com.linagora.tmail.james.jmap.model.DisplayName; import com.linagora.tmail.james.jmap.model.Label; import com.linagora.tmail.james.jmap.model.LabelCreationRequest; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanId; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanRepositoryContract; import io.restassured.RestAssured; import io.restassured.specification.RequestSpecification; @@ -138,28 +135,6 @@ void shouldMigratePGPPublicKeys(GuiceJamesServer server) throws Exception { .isEmpty(); } - @Test - void shouldAdaptRateLimiting(GuiceJamesServer server) { - RateLimitingProbe rateLimitingProbe = server.getProbe(RateLimitingProbe.class); - RateLimitingPlanId planId = rateLimitingProbe.createPlan(RateLimitingPlanRepositoryContract.CREATION_REQUEST()).id(); - - rateLimitingProbe.applyPlan(ALICE, planId); - - String taskId = webAdminApi - .queryParam("action", "rename") - .post("/users/" + ALICE.asString() + "/rename/" + BOB.asString()) - .jsonPath() - .get("taskId"); - - webAdminApi.get("/tasks/" + taskId + "/await") - .then() - .statusCode(HttpStatus.SC_OK) - .body("additionalInformation.status.RateLimitingPlanUsernameChangeTaskStep", Matchers.is("DONE")); - - assertThat(rateLimitingProbe.listUsersOfAPlan(planId)) - .containsExactly(BOB); - } - @Test void shouldMigrateLabels(GuiceJamesServer server) { JmapGuiceLabelProbe labelProbe = server.getProbe(JmapGuiceLabelProbe.class); diff --git a/tmail-backend/integration-tests/webadmin/webadmin-integration-tests-common/src/main/java/com/linagora/tmail/integration/probe/RateLimitingProbe.java b/tmail-backend/integration-tests/webadmin/webadmin-integration-tests-common/src/main/java/com/linagora/tmail/integration/probe/RateLimitingProbe.java deleted file mode 100644 index 90115f05fe..0000000000 --- a/tmail-backend/integration-tests/webadmin/webadmin-integration-tests-common/src/main/java/com/linagora/tmail/integration/probe/RateLimitingProbe.java +++ /dev/null @@ -1,60 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.integration.probe; - -import java.util.List; - -import jakarta.inject.Inject; - -import org.apache.james.core.Username; -import org.apache.james.utils.GuiceProbe; - -import com.linagora.tmail.rate.limiter.api.RateLimitingPlan; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanCreateRequest; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanId; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanRepository; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanUserRepository; - -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -public class RateLimitingProbe implements GuiceProbe { - private final RateLimitingPlanRepository planRepository; - private final RateLimitingPlanUserRepository planUserRepository; - - @Inject - public RateLimitingProbe(RateLimitingPlanRepository planRepository, RateLimitingPlanUserRepository planUserRepository) { - this.planRepository = planRepository; - this.planUserRepository = planUserRepository; - } - - public RateLimitingPlan createPlan(RateLimitingPlanCreateRequest request) { - return Mono.from(planRepository.create(request)).block(); - } - - public void applyPlan(Username username, RateLimitingPlanId planId) { - Mono.from(planUserRepository.applyPlan(username, planId)).block(); - } - - public List listUsersOfAPlan(RateLimitingPlanId planId) { - return Flux.from(planUserRepository.listUsers(planId)) - .collectList() - .block(); - } -} diff --git a/tmail-backend/mailets/pom.xml b/tmail-backend/mailets/pom.xml index f90697ab1c..f27218cbcf 100644 --- a/tmail-backend/mailets/pom.xml +++ b/tmail-backend/mailets/pom.xml @@ -116,17 +116,6 @@ james-server-queue-api test - - ${james.groupId} - james-server-rate-limiter-redis - test - - - ${james.groupId} - james-server-rate-limiter-redis - test - test-jar - ${james.groupId} james-server-testing diff --git a/tmail-backend/mailets/src/main/scala/com/linagora/tmail/mailets/EnforceRateLimitingPlan.scala b/tmail-backend/mailets/src/main/scala/com/linagora/tmail/mailets/EnforceRateLimitingPlan.scala deleted file mode 100644 index 8f77795ae4..0000000000 --- a/tmail-backend/mailets/src/main/scala/com/linagora/tmail/mailets/EnforceRateLimitingPlan.scala +++ /dev/null @@ -1,166 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.mailets - -import java.time.Duration -import java.time.temporal.ChronoUnit - -import com.google.common.annotations.VisibleForTesting -import com.google.common.base.Preconditions -import com.google.common.collect.ImmutableList -import com.linagora.tmail.mailets.EnforceRateLimitingPlan.{ACCEPTABLE_OPERATIONS, LIMIT_PER_RECIPIENTS_OPERATIONS, LIMIT_PER_SENDER_OPERATIONS} -import com.linagora.tmail.rate.limiter.api.OperationLimitations.{DELIVERY_LIMITATIONS_NAME, RELAY_LIMITATIONS_NAME, TRANSIT_LIMITATIONS_NAME} -import com.linagora.tmail.rate.limiter.api.{CacheRateLimitingPlan, OperationLimitations, RateLimitingPlanId, RateLimitingPlanNotFoundException, RateLimitingPlanRepository, RateLimitingPlanUserRepository} -import jakarta.inject.Inject -import org.apache.james.core.{MailAddress, Username} -import org.apache.james.lifecycle.api.LifecycleUtil -import org.apache.james.metrics.api.GaugeRegistry -import org.apache.james.rate.limiter.api.{AcceptableRate, RateExceeded, RateLimiterFactory, RateLimitingResult, Rule, Rules} -import org.apache.james.transport.mailets.ConfigurationOps.DurationOps -import org.apache.james.transport.mailets.KeyPrefix -import org.apache.james.util.DurationParser -import org.apache.mailet.Mail -import org.apache.mailet.base.GenericMailet -import reactor.core.scala.publisher.{SFlux, SMono} - -import scala.jdk.CollectionConverters._ -import scala.util.Using - -object EnforceRateLimitingPlan { - val LIMIT_PER_SENDER_OPERATIONS: Set[String] = Set(TRANSIT_LIMITATIONS_NAME, RELAY_LIMITATIONS_NAME) - val LIMIT_PER_RECIPIENTS_OPERATIONS: Set[String] = Set(DELIVERY_LIMITATIONS_NAME) - val ACCEPTABLE_OPERATIONS: Set[String] = LIMIT_PER_SENDER_OPERATIONS ++ LIMIT_PER_RECIPIENTS_OPERATIONS -} - -class EnforceRateLimitingPlan @Inject()(planRepository: RateLimitingPlanRepository, - planUserRepository: RateLimitingPlanUserRepository, - rateLimiterFactory: RateLimiterFactory, - gaugeRegistry: GaugeRegistry) extends GenericMailet { - - private var operationLimitation: String = _ - private var exceededProcessor: String = _ - private var rateLimitSender: Boolean = _ - private var rateLimitRecipient: Boolean = _ - private var planRateLimiterResolver: PlanRateLimiterResolver = _ - private var planStore: RateLimitingPlanRepository = _ - - override def init(): Unit = { - exceededProcessor = getInitParameter("exceededProcessor", Mail.ERROR) - operationLimitation = getInitParameter("operationLimitation") - rateLimitSender = LIMIT_PER_SENDER_OPERATIONS.contains(operationLimitation) - rateLimitRecipient = LIMIT_PER_RECIPIENTS_OPERATIONS.contains(operationLimitation) - Preconditions.checkArgument(operationLimitation != null && operationLimitation.nonEmpty, "`operationLimitation` is compulsory".asInstanceOf[Object]) - Preconditions.checkArgument(ACCEPTABLE_OPERATIONS.contains(operationLimitation), s"`operationLimitation` must be [${String.join(", ", ACCEPTABLE_OPERATIONS.asJava)}]".asInstanceOf[Object]) - - planStore = parseCacheExpiration() - .map(duration => new CacheRateLimitingPlan(planRepository, duration, gaugeRegistry, Some(operationLimitation))) - .getOrElse(planRepository) - - planRateLimiterResolver = PlanRateLimiterResolver( - rateLimiterFactory = rateLimiterFactory, - keyPrefix = Option(getInitParameter("keyPrefix")).map(KeyPrefix), - precision = getMailetConfig.getDuration("precision")) - } - - @VisibleForTesting - def parseCacheExpiration(): Option[Duration] = Option(getInitParameter("cacheExpiration")) - .map(string => DurationParser.parse(string, ChronoUnit.SECONDS)) - .map(duration => { - Preconditions.checkArgument(!duration.isZero && !duration.isNegative, "`cacheExpiration` must be positive".asInstanceOf[Object]) - duration - }) - - override def service(mail: Mail): Unit = - if (rateLimitSender) { - mail.getMaybeSender.asOptional() - .ifPresent(sender => applyRateLimiterPerSender(mail, Username.fromMailAddress(sender))) - } else if (rateLimitRecipient && !mail.getRecipients.isEmpty) { - applyRateLimiterPerRecipient(mail) - } - - private def applyRateLimiter(mail: Mail, username: Username): SMono[RateLimitingResult] = - SMono.fromPublisher(planUserRepository.getPlanByUser(username)) - .flatMap(retrieveRateLimiter) - .flatMapMany(SFlux.fromIterable) - .flatMap(_.rateLimit(username, mail)) - .fold[RateLimitingResult](AcceptableRate)((a, b) => a.merge(b)) - .onErrorResume { - case _: RateLimitingPlanNotFoundException => SMono.just[RateLimitingResult](AcceptableRate) - } - - private def retrieveRateLimiter(id: RateLimitingPlanId): SMono[Seq[TmailPlanRateLimiter]] = - SMono.fromPublisher(planStore.get(id)) - .flatMapIterable(_.operationLimitations) - .filter(_.asString().equals(operationLimitation)) - .next() - .map(planRateLimiterResolver.extractRateLimiters(id, _)) - - private def applyRateLimiterPerSender(mail: Mail, username: Username): Unit = { - val rateLimitingResult: RateLimitingResult = applyRateLimiter(mail, username).block() - if (rateLimitingResult.equals(RateExceeded)) { - mail.setState(exceededProcessor) - } - } - - private def applyRateLimiterPerRecipient(mail: Mail): Unit = { - val rateLimitResults: Seq[(MailAddress, RateLimitingResult)] = SFlux.fromIterable(mail.getRecipients.asScala) - .flatMap(recipient => applyRateLimiter(mail, Username.fromMailAddress(recipient)) - .map(rateLimitingResult => recipient -> rateLimitingResult)) - .collectSeq() - .block() - - val rateLimitedRecipients: Seq[MailAddress] = rateLimitResults.filter(_._2.equals(RateExceeded)).map(_._1) - val acceptableRecipients: Seq[MailAddress] = rateLimitResults.filter(_._2.equals(AcceptableRate)).map(_._1) - - (acceptableRecipients, rateLimitedRecipients) match { - case (acceptable, _) if acceptable.isEmpty => mail.setState(exceededProcessor) - case (_, exceeded) if exceeded.isEmpty => // do nothing - case _ => - mail.setRecipients(ImmutableList.copyOf(acceptableRecipients.asJava)) - - Using(mail.duplicate())(newMail => { - newMail.setRecipients(ImmutableList.copyOf(rateLimitedRecipients.asJava)) - getMailetContext.sendMail(newMail, exceededProcessor) - })(LifecycleUtil.dispose(_)) - } - } -} - -case class PlanRateLimiterResolver(rateLimiterFactory: RateLimiterFactory, - keyPrefix: Option[KeyPrefix], - precision: Option[Duration]) { - - def extractRateLimiters(planId: RateLimitingPlanId, operationLimitations: OperationLimitations): Seq[TmailPlanRateLimiter] = - aggregateRule(operationLimitations) - .map(pair => TmailPlanRateLimiter( - rateLimiter = rateLimiterFactory.withSpecification(pair._2, precision), - limitTypeName = pair._1, - keyPrefix = keyPrefix, - planId = planId, - operationLimitationName = operationLimitations.asString())) - .toSeq - - private def aggregateRule(operationLimitations: OperationLimitations): Map[String, Rules] = - operationLimitations.rateLimitations() - .flatMap(rateLimitation => rateLimitation.limits.value - .map(limit => limit -> rateLimitation.period)) - .groupBy(_._1.asString()) - .map(pair => pair._1 -> Rules(pair._2.map(pair => Rule(pair._1.allowedQuantity(), pair._2)))) - -} diff --git a/tmail-backend/mailets/src/main/scala/com/linagora/tmail/mailets/TmailPlanRateLimiter.scala b/tmail-backend/mailets/src/main/scala/com/linagora/tmail/mailets/TmailPlanRateLimiter.scala deleted file mode 100644 index be3b5ca9b4..0000000000 --- a/tmail-backend/mailets/src/main/scala/com/linagora/tmail/mailets/TmailPlanRateLimiter.scala +++ /dev/null @@ -1,71 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.mailets - -import com.linagora.tmail.rate.limiter.api.LimitTypes.{COUNT, SIZE} -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanId -import eu.timepit.refined.auto._ -import org.apache.james.core.Username -import org.apache.james.rate.limiter.api.Increment.Increment -import org.apache.james.rate.limiter.api.{AcceptableRate, Increment, RateLimiter, RateLimitingKey, RateLimitingResult} -import org.apache.james.transport.mailets.KeyPrefix -import org.apache.mailet.Mail -import org.reactivestreams.Publisher -import reactor.core.scala.publisher.SMono - -case class TmailPlanRateLimiter(rateLimiter: RateLimiter, - keyPrefix: Option[KeyPrefix] = None, - limitTypeName: String, - planId: RateLimitingPlanId, - operationLimitationName: String) { - - def rateLimit(username: Username, mail: Mail): Publisher[RateLimitingResult] = - LimitTypeUtils.extractQuantity(mail, limitTypeName) - .map(increment => rateLimiter.rateLimit( - key = RateLimitingPlanKey( - keyPrefix, - limitTypeName, - planId, - operationLimitationName, - username), - increaseQuantity = increment)) - .getOrElse(SMono.just[RateLimitingResult](AcceptableRate)) -} - -case class RateLimitingPlanKey(keyPrefix: Option[KeyPrefix], - limitTypeName: String, - planId: RateLimitingPlanId, - operationLimitationName: String, - username: Username) extends RateLimitingKey { - override def asString(): String = s"${ - keyPrefix.map(prefix => prefix.value + "_") - .getOrElse("") - }${planId.value.toString}_${operationLimitationName}_${limitTypeName}_${username.asString()}" -} - -object LimitTypeUtils { - def extractQuantity(mail: Mail, limitTypeName: String): Option[Increment] = - limitTypeName match { - case COUNT => Some(1) - case SIZE => Increment.validate(mail.getMessageSize.toInt) match { - case Right(size) => Some(size) - case Left(e) => throw e - } - } -} diff --git a/tmail-backend/mailets/src/test/scala/com/linagora/tmail/mailets/EnforceRateLimitingPlanTest.scala b/tmail-backend/mailets/src/test/scala/com/linagora/tmail/mailets/EnforceRateLimitingPlanTest.scala deleted file mode 100644 index e04cdde327..0000000000 --- a/tmail-backend/mailets/src/test/scala/com/linagora/tmail/mailets/EnforceRateLimitingPlanTest.scala +++ /dev/null @@ -1,728 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.mailets - -import java.time.Duration -import java.util -import java.util.stream.IntStream - -import com.codahale.metrics.MetricRegistry -import com.linagora.tmail.mailets.EnforceRateLimitingPlanTest.{USER1, USER2} -import com.linagora.tmail.rate.limiter.api.memory.MemoryRateLimitingPlanUserRepository -import com.linagora.tmail.rate.limiter.api.{DeliveryLimitations, InMemoryRateLimitingPlanRepository, LimitTypes, OperationLimitationsType, RateLimitation, RateLimitingPlan, RateLimitingPlanCreateRequest, RateLimitingPlanRepository, RateLimitingPlanUserRepository, RelayLimitations, TransitLimitations} -import eu.timepit.refined.auto._ -import org.apache.james.backends.redis.{DockerRedis, RedisClientFactory, RedisExtension, StandaloneRedisConfiguration} -import org.apache.james.core.Username -import org.apache.james.metrics.api.NoopGaugeRegistry -import org.apache.james.metrics.dropwizard.DropWizardGaugeRegistry -import org.apache.james.rate.limiter.redis.RedisRateLimiterFactory -import org.apache.james.server.core.filesystem.FileSystemImpl -import org.apache.james.util.Size.parse -import org.apache.mailet.base.test.{FakeMail, FakeMailContext, FakeMailetConfig} -import org.apache.mailet.{Mail, MailetConfig} -import org.assertj.core.api.Assertions.{assertThat, assertThatCode, assertThatThrownBy} -import org.assertj.core.api.SoftAssertions.assertSoftly -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.{BeforeEach, Nested, Test} -import reactor.core.scala.publisher.SMono - -object EnforceRateLimitingPlanTest { - val USER1: Username = Username.of("user1@domain.tld") - val USER2: Username = Username.of("user2@domain.tld") -} - -@ExtendWith(Array(classOf[RedisExtension])) -class EnforceRateLimitingPlanTest { - - var rateLimitationPlanRepository: RateLimitingPlanRepository = _ - var rateLimitingPlanUserRepository: RateLimitingPlanUserRepository = _ - var redisRateLimiterFactory: RedisRateLimiterFactory = _ - - def testee(mailetConfig: MailetConfig): EnforceRateLimitingPlan = { - val mailet: EnforceRateLimitingPlan = new EnforceRateLimitingPlan(rateLimitationPlanRepository, rateLimitingPlanUserRepository, redisRateLimiterFactory, - new NoopGaugeRegistry()) - mailet.init(mailetConfig) - mailet - } - - @BeforeEach - def setup(redis: DockerRedis): Unit = { - val redisConfiguration: StandaloneRedisConfiguration = StandaloneRedisConfiguration.from(redis.redisURI().toString) - rateLimitationPlanRepository = new InMemoryRateLimitingPlanRepository - rateLimitingPlanUserRepository = new MemoryRateLimitingPlanUserRepository - redisRateLimiterFactory = new RedisRateLimiterFactory(redisConfiguration, new RedisClientFactory(FileSystemImpl.forTesting, redisConfiguration)) - - val rateLimitingPlan: RateLimitingPlan = SMono.fromPublisher(rateLimitationPlanRepository.create(RateLimitingPlanCreateRequest( - name = "PaidPlan", - operationLimitations = OperationLimitationsType.liftOrThrow(Seq( - TransitLimitations(Seq( - RateLimitation(name = "transit limit 1", - period = Duration.ofSeconds(5), - limits = LimitTypes.from(Map(("count", 1)))))), - RelayLimitations(Seq( - RateLimitation(name = "relay limit 1", - period = Duration.ofSeconds(5), - limits = LimitTypes.from(Map(("count", 1)))))), - DeliveryLimitations(Seq( - RateLimitation(name = "delivery limit 1", - period = Duration.ofSeconds(5), - limits = LimitTypes.from(Map(("count", 1))))))))))) - .block() - - SMono.fromPublisher(rateLimitingPlanUserRepository.applyPlan(USER1, rateLimitingPlan.id)).block() - SMono.fromPublisher(rateLimitingPlanUserRepository.applyPlan(USER2, rateLimitingPlan.id)).block() - } - - @Nested - class PerSenderLimit { - var mailetConfig: FakeMailetConfig.Builder = _ - var mailet: EnforceRateLimitingPlan = _ - - @BeforeEach - def setUpSender(): Unit = { - mailetConfig = FakeMailetConfig.builder() - .mailetName("EnforceRateLimitingPlan") - .setProperty("operationLimitation", "TransitLimitations") - .setProperty("precision", "1s") - mailet = testee(mailetConfig.build()) - } - - @Test - def shouldFlowToTheIntendedProcessor(): Unit = { - val mailet: EnforceRateLimitingPlan = testee(mailetConfig - .setProperty("exceededProcessor", "tooMuchMails") - .build()) - - val mail1: Mail = FakeMail.builder() - .name("mail1") - .sender(USER1.asString()) - .recipients("rcpt1@linagora.com") - .state("transport") - .build() - - val mail2: Mail = FakeMail.builder() - .name("mail2") - .sender(USER1.asString()) - .recipients("rcpt2@linagora.com") - .state("transport") - .build() - - mailet.service(mail1) - mailet.service(mail2) - - assertSoftly(softly => { - softly.assertThat(mail1.getState).isEqualTo("transport") - softly.assertThat(mail2.getState).isEqualTo("tooMuchMails") - }) - } - - @Test - def shouldNotAppliedToRecipientDoesNotInPlan(): Unit = { - val sender: String = "notInPlan@domain.tld" - - val mail1: Mail = FakeMail.builder() - .name("mail1") - .sender(sender) - .recipients("rcpt1@linagora.com") - .state("transport") - .build() - - val mail2: Mail = FakeMail.builder() - .name("mail2") - .sender(sender) - .recipients("rcpt2@linagora.com") - .state("transport") - .build() - - mailet.service(mail1) - mailet.service(mail2) - - assertSoftly(softly => { - softly.assertThat(mail1.getState).isEqualTo("transport") - softly.assertThat(mail2.getState).isEqualTo("transport") - }) - } - - @Test - def shouldBeAppliedPerSender(): Unit = { - val mail1: Mail = FakeMail.builder() - .name("mail1") - .sender(USER1.asString()) - .recipients("rcpt1@linagora.com") - .state("transport") - .build() - - val mail2: Mail = FakeMail.builder() - .name("mail2") - .sender(USER2.asString()) - .recipients("rcpt2@linagora.com") - .state("transport") - .build() - - mailet.service(mail1) - mailet.service(mail2) - - assertSoftly(softly => { - softly.assertThat(mail1.getState).isEqualTo("transport") - softly.assertThat(mail2.getState).isEqualTo("transport") - }) - } - - @Test - def shouldRateLimitSizeOfEmails(): Unit = { - val rateLimitingPlan: RateLimitingPlan = SMono.fromPublisher(rateLimitationPlanRepository.create(RateLimitingPlanCreateRequest( - name = "LimitPlan", - operationLimitations = OperationLimitationsType.liftOrThrow(Seq( - TransitLimitations(Seq(RateLimitation( - name = "transit limit 1", - period = Duration.ofSeconds(100), - limits = LimitTypes.from(Map(("size", parse("100K").asBytes()))))))))))) - .block() - - val username: Username = Username.of("user3@domain.tld") - SMono.fromPublisher(rateLimitingPlanUserRepository.applyPlan(username, rateLimitingPlan.id)).block() - mailet = testee(mailetConfig.build()) - - val mail1: Mail = FakeMail.builder() - .name("mail1") - .sender(username.asString()) - .recipients("rcpt1@linagora.com") - .size(50 * 1024) - .state("transport") - .build() - - val mail2: Mail = FakeMail.builder() - .name("mail2") - .sender(username.asString()) - .recipients("rcpt1@linagora.com") - .size(51 * 1024) - .state("transport") - .build() - - val mail3: Mail = FakeMail.builder() - .name("mail3") - .sender(username.asString()) - .recipients("rcpt1@linagora.com") - .size(49 * 1024) - .state("transport") - .build() - - mailet.service(mail1) - mailet.service(mail2) - mailet.service(mail3) - - assertSoftly(softly => { - softly.assertThat(mail1.getState).isEqualTo("transport") - softly.assertThat(mail2.getState).isEqualTo("error") - softly.assertThat(mail3.getState).isEqualTo("transport") - }) - } - - @Test - def severalLimitsShouldBeApplied(): Unit = { - val rateLimitingPlan: RateLimitingPlan = SMono.fromPublisher(rateLimitationPlanRepository.create(RateLimitingPlanCreateRequest( - name = "LimitPlan", - operationLimitations = OperationLimitationsType.liftOrThrow(Seq( - TransitLimitations(Seq(RateLimitation( - name = "transit limit 1", - period = Duration.ofSeconds(100), - limits = LimitTypes.from(Map(("count", 3), ("size", parse("100K").asBytes()))))))))))) - .block() - - val username: Username = Username.of("user3@domain.tld") - SMono.fromPublisher(rateLimitingPlanUserRepository.applyPlan(username, rateLimitingPlan.id)).block() - mailet = testee(mailetConfig.build()) - - val mail1: Mail = FakeMail.builder() - .name("mail1") - .sender(username.asString()) - .recipients("rcpt1@linagora.com") - .size(50 * 1024) - .state("transport") - .build() - - // Will exceed the size rate limit - val mail2: Mail = FakeMail.builder() - .name("mail2") - .sender(username.asString()) - .recipients("rcpt2@linagora.com") - .size(60 * 1024) - .state("transport") - .build() - - val mail3: Mail = FakeMail.builder() - .name("mail4") - .sender(username.asString()) - .recipients("rcpt3@linagora.com") - .size(1) - .state("transport") - .build() - - mailet.service(mail1) - mailet.service(mail2) - mailet.service(mail3) - - assertSoftly(softly => { - softly.assertThat(mail1.getState).isEqualTo("transport") - softly.assertThat(mail2.getState).isEqualTo("error") - softly.assertThat(mail3.getState).isEqualTo("transport") - }) - } - - @Test - def shouldNotAppliedWhenOperationLimitationNotFound(): Unit = { - val rateLimitingPlan: RateLimitingPlan = SMono.fromPublisher(rateLimitationPlanRepository.create(RateLimitingPlanCreateRequest( - name = "LimitPlan", - operationLimitations = OperationLimitationsType.liftOrThrow(Seq( - DeliveryLimitations(Seq(RateLimitation( - name = "delivery limit 1", - period = Duration.ofSeconds(100), - limits = LimitTypes.from(Map(("count", 1))))))))))) - .block() - - val username: Username = Username.of("user3@domain.tld") - SMono.fromPublisher(rateLimitingPlanUserRepository.applyPlan(username, rateLimitingPlan.id)).block() - - val mail1: Mail = FakeMail.builder() - .name("mail1") - .sender(username.asString()) - .recipients("rcpt1@linagora.com") - .state("transport") - .build() - - val mail2: Mail = FakeMail.builder() - .name("mail2") - .sender(username.asString()) - .recipients("rcpt2@linagora.com") - .state("transport") - .build() - - mailet.service(mail1) - mailet.service(mail2) - - assertSoftly(softly => { - softly.assertThat(mail1.getState).isEqualTo("transport") - softly.assertThat(mail2.getState).isEqualTo("transport") - }) - } - - @Test - def relayLimitationsShouldRateLimitPerSender(): Unit = { - val mailet: EnforceRateLimitingPlan = testee(FakeMailetConfig.builder() - .mailetName("EnforceRateLimitingPlan") - .setProperty("operationLimitation", "RelayLimitations") - .setProperty("precision", "1s") - .build()) - - val mail1: Mail = FakeMail.builder() - .name("mail1") - .sender(USER1.asString()) - .recipients("rcpt1@linagora.com") - .state("transport") - .build() - - val mail2: Mail = FakeMail.builder() - .name("mail2") - .sender(USER1.asString()) - .recipients("rcpt2@linagora.com") - .state("transport") - .build() - - mailet.service(mail1) - mailet.service(mail2) - - assertSoftly(softly => { - softly.assertThat(mail1.getState).isEqualTo("transport") - softly.assertThat(mail2.getState).isEqualTo("error") - }) - } - } - - @Nested - class PerRecipientLimit { - var mailetConfig: FakeMailetConfig.Builder = _ - var mailet: EnforceRateLimitingPlan = _ - - @BeforeEach - def setUpSender(): Unit = { - mailetConfig = FakeMailetConfig.builder() - .mailetName("EnforceRateLimitingPlan") - .setProperty("operationLimitation", "DeliveryLimitations") - .setProperty("precision", "1s") - mailet = testee(mailetConfig.build()) - } - - @Test - def shouldFlowToTheIntendedProcessor(): Unit = { - val mailet: EnforceRateLimitingPlan = testee(mailetConfig - .setProperty("exceededProcessor", "tooMuchMails") - .build()) - - val mail1: Mail = FakeMail.builder() - .name("mail") - .sender("sender1@domain.tld") - .recipients(USER1.asString()) - .state("transport") - .build() - - val mail2: Mail = FakeMail.builder() - .name("mail2") - .sender("sender1@domain.tld") - .recipients(USER1.asString()) - .state("transport") - .build() - - mailet.service(mail1) - mailet.service(mail2) - - assertSoftly(softly => { - softly.assertThat(mail1.getState).isEqualTo("transport") - softly.assertThat(mail2.getState).isEqualTo("tooMuchMails") - }) - } - - @Test - def shouldNotAppliedToRecipientDoesNotInPlan(): Unit = { - val recipient: String = "notInPlan@domain.tld" - - val mail1: Mail = FakeMail.builder() - .name("mail") - .sender("sender1@domain.tld") - .recipients(recipient) - .state("transport") - .build() - - val mail2: Mail = FakeMail.builder() - .name("mail2") - .sender("sender1@domain.tld") - .recipients(recipient) - .state("transport") - .build() - - mailet.service(mail1) - mailet.service(mail2) - - assertSoftly(softly => { - softly.assertThat(mail1.getState).isEqualTo("transport") - softly.assertThat(mail2.getState).isEqualTo("transport") - }) - } - - @Test - def shouldBeAppliedPerRecipient(): Unit = { - val mailet: EnforceRateLimitingPlan = testee(mailetConfig.build()) - - val mail1: Mail = FakeMail.builder() - .name("mail") - .sender("sender1@domain.tld") - .recipients(USER1.asString()) - .state("transport") - .build() - - val mail2: Mail = FakeMail.builder() - .name("mail2") - .sender("sender1@domain.tld") - .recipients(USER2.asString()) - .state("transport") - .build() - - mailet.service(mail1) - mailet.service(mail2) - - assertSoftly(softly => { - softly.assertThat(mail1.getState).isEqualTo("transport") - softly.assertThat(mail2.getState).isEqualTo("transport") - }) - } - - @Test - def shouldNotBeAppliedWhenDoNotHaveRecipient(): Unit = { - val mail: Mail = FakeMail.builder() - .name("mail") - .sender("sender1@domain.tld") - .state("transport") - .build() - - mailet.service(mail) - assertThat(mail.getState).isEqualTo("transport") - } - - @Test - def shouldNotBeAppliedPerSender(): Unit = { - val mail1: Mail = FakeMail.builder() - .name("mail") - .sender("sender1@domain.tld") - .recipients(USER1.asString()) - .state("transport") - .build() - - val mail2: Mail = FakeMail.builder() - .name("mail2") - .sender("sender2@domain.tld") - .recipients(USER1.asString()) - .state("transport") - .build() - - mailet.service(mail1) - mailet.service(mail2) - - assertSoftly(softly => { - softly.assertThat(mail1.getState).isEqualTo("transport") - softly.assertThat(mail2.getState).isEqualTo("error") - }) - } - - @Test - def shouldRateLimitSizeOfEmails(): Unit = { - val rateLimitingPlan: RateLimitingPlan = SMono.fromPublisher(rateLimitationPlanRepository.create(RateLimitingPlanCreateRequest( - name = "LimitPlan", - operationLimitations = OperationLimitationsType.liftOrThrow(Seq( - DeliveryLimitations(Seq(RateLimitation( - name = "transit limit 1", - period = Duration.ofSeconds(100), - limits = LimitTypes.from(Map(("size", parse("100K").asBytes()))))))))))) - .block() - - val username: Username = Username.of("user3@domain.tld") - SMono.fromPublisher(rateLimitingPlanUserRepository.applyPlan(username, rateLimitingPlan.id)).block() - - val mail1: Mail = FakeMail.builder() - .name("mail1") - .sender("sender@domain.tld") - .recipients(username.asString()) - .size(50 * 1024) - .state("transport") - .build() - - val mail2: Mail = FakeMail.builder() - .name("mail2") - .sender("sender@domain.tld") - .recipients(username.asString()) - .size(51 * 1024) - .state("transport") - .build() - - val mail3: Mail = FakeMail.builder() - .name("mail3") - .sender("sender@domain.tld") - .recipients(username.asString()) - .size(49 * 1024) - .state("transport") - .build() - - mailet.service(mail1) - mailet.service(mail2) - mailet.service(mail3) - - assertSoftly(softly => { - softly.assertThat(mail1.getState).isEqualTo("transport") - softly.assertThat(mail2.getState).isEqualTo("error") - softly.assertThat(mail3.getState).isEqualTo("transport") - }) - } - - @Test - def shouldRateLimitPerRecipient(): Unit = { - val mailetContext: FakeMailContext = FakeMailContext.defaultContext - val mailet: EnforceRateLimitingPlan = testee(mailetConfig - .mailetContext(mailetContext).build()) - - val mail1: Mail = FakeMail.builder() - .name("mail") - .sender("sender1@domain.tld") - .recipients(USER1.asString()) - .state("transport") - .build() - - val mail2: Mail = FakeMail.builder() - .name("mail") - .sender("sender1@domain.tld") - .recipients(USER1.asString(), USER2.asString()) - .state("transport") - .build() - - mailet.service(mail1) - mailet.service(mail2) - - assertSoftly(softly => { - softly.assertThat(mail1.getState).isEqualTo("transport") - softly.assertThat(mail2.getState).isEqualTo("transport") - }) - - val sentMails: util.List[FakeMailContext.SentMail] = mailetContext.getSentMails - assertThat(sentMails).hasSize(1) - assertSoftly(softly => { - softly.assertThat(sentMails.get(0).getState).isEqualTo("error") - softly.assertThat(sentMails.get(0).getRecipients).containsExactlyInAnyOrder(USER1.asMailAddress()) - }) - } - - @Test - def shouldRateLimitedWhenAllRecipientsExceeded(): Unit = { - val mail1: Mail = FakeMail.builder() - .name("mail") - .sender("sender1@domain.tld") - .recipients(USER1.asString(), USER2.asString()) - .state("transport") - .build() - - val mail2: Mail = FakeMail.builder() - .name("mail") - .sender("sender1@domain.tld") - .recipients(USER1.asString(), USER2.asString()) - .state("transport") - .build() - - mailet.service(mail1) - mailet.service(mail2) - - assertSoftly(softly => { - softly.assertThat(mail1.getState).isEqualTo("transport") - softly.assertThat(mail2.getState).isEqualTo("error") - }) - } - - @Test - def shouldNotAppliedWhenOperationLimitationNotFound(): Unit = { - val rateLimitingPlan: RateLimitingPlan = SMono.fromPublisher(rateLimitationPlanRepository.create(RateLimitingPlanCreateRequest( - name = "LimitPlan", - operationLimitations = OperationLimitationsType.liftOrThrow(Seq( - TransitLimitations(Seq(RateLimitation( - name = "transit limit 1", - period = Duration.ofSeconds(100), - limits = LimitTypes.from(Map(("count", 1))))))))))) - .block() - - val username: Username = Username.of("user3@domain.tld") - SMono.fromPublisher(rateLimitingPlanUserRepository.applyPlan(username, rateLimitingPlan.id)).block() - - val mail1: Mail = FakeMail.builder() - .name("mail") - .sender("sender1@domain.tld") - .recipients(username.asString()) - .state("transport") - .build() - - val mail2: Mail = FakeMail.builder() - .name("mail2") - .sender("sender1@domain.tld") - .recipients(username.asString()) - .state("transport") - .build() - - mailet.service(mail1) - mailet.service(mail2) - - assertSoftly(softly => { - softly.assertThat(mail1.getState).isEqualTo("transport") - softly.assertThat(mail2.getState).isEqualTo("transport") - }) - } - } - - @Nested - class Configuration { - @Test - def shouldFailWhenNoOperationLimitation(): Unit = { - assertThatThrownBy(() => testee(FakeMailetConfig.builder() - .mailetName("EnforceRateLimitingPlan") - .build())) - .isInstanceOf(classOf[IllegalArgumentException]) - } - - @Test - def shouldFailWhenInvalidOperationLimitation(): Unit = { - assertThatThrownBy(() => testee(FakeMailetConfig.builder() - .mailetName("EnforceRateLimitingPlan") - .setProperty("operationLimitation", "badOperation") - .build())) - .isInstanceOf(classOf[IllegalArgumentException]) - } - - @Test - def shouldNotFailWhenValidOperationLimitation(): Unit = { - assertThatCode(() => testee(FakeMailetConfig.builder() - .mailetName("EnforceRateLimitingPlan") - .setProperty("operationLimitation", "TransitLimitations") - .build())) - .doesNotThrowAnyException() - } - - @Test - def cacheExpirationShouldSupportUnits(): Unit = { - assertThat(testee(FakeMailetConfig.builder() - .mailetName("EnforceRateLimitingPlan") - .setProperty("operationLimitation", "TransitLimitations") - .setProperty("cacheExpiration", "2h") - .build()).parseCacheExpiration()) - .isEqualTo(Some(Duration.ofHours(2))) - } - - - @Test - def cacheExpirationWithNoUnitShouldDefaultToSeconds(): Unit = { - assertThat(testee(FakeMailetConfig.builder() - .mailetName("EnforceRateLimitingPlan") - .setProperty("operationLimitation", "TransitLimitations") - .setProperty("cacheExpiration", "2") - .build()).parseCacheExpiration()) - .isEqualTo(Some(Duration.ofSeconds(2))) - } - - @Test - def shouldFailWhenBadCacheExpiration(): Unit = { - assertThatThrownBy(() => testee(FakeMailetConfig.builder() - .mailetName("EnforceRateLimitingPlan") - .setProperty("operationLimitation", "TransitLimitations") - .setProperty("cacheExpiration", "bad") - .build()).parseCacheExpiration()) - .isInstanceOf(classOf[IllegalArgumentException]) - } - } - - @Test - def planShouldBeCachedWhenConfigurationIsProvided(): Unit = { - val metricRegistry: MetricRegistry = new MetricRegistry() - val gaugeRegistry: DropWizardGaugeRegistry = new DropWizardGaugeRegistry(metricRegistry) - - val mailet: EnforceRateLimitingPlan = new EnforceRateLimitingPlan(rateLimitationPlanRepository, rateLimitingPlanUserRepository, redisRateLimiterFactory, - gaugeRegistry) - - mailet.init(FakeMailetConfig.builder() - .mailetName("EnforceRateLimitingPlan") - .setProperty("operationLimitation", "TransitLimitations") - .setProperty("precision", "1s") - .setProperty("cacheExpiration", "2m") - .build()) - - val sentCount: Int = 10; - IntStream.range(0, sentCount) - .forEach(index => { - val mail: Mail = FakeMail.builder() - .name("mail" + index) - .sender(USER1.asString()) - .recipients("rcpt1@linagora.com") - .state("transport") - .build() - mailet.service(mail) - }) - assertThat(metricRegistry.getGauges.get("TransitLimitations.rate_limiting_plan.cache.get.hitCount").getValue) - .isEqualTo(java.lang.Long.valueOf(sentCount - 1)) - } -} diff --git a/tmail-backend/rate-limiter/rate-limiter-api/pom.xml b/tmail-backend/rate-limiter/rate-limiter-api/pom.xml index e58a7c202d..792325d446 100644 --- a/tmail-backend/rate-limiter/rate-limiter-api/pom.xml +++ b/tmail-backend/rate-limiter/rate-limiter-api/pom.xml @@ -33,63 +33,18 @@ Twake Mail :: Rate Limiter :: Api - - ${james.groupId} - apache-mailet-api - - - ${james.groupId} - james-core - ${james.groupId} james-server-data-api - - ${james.groupId} - james-server-rate-limiter - - - ${james.groupId} - metrics-api - ${james.groupId} testing-base test - - com.github.ben-manes.caffeine - caffeine - com.google.inject guice - - io.projectreactor - reactor-scala-extensions_${scala.base} - - - eu.timepit - refined_${scala.base} - - - - - - net.alchim31.maven - scala-maven-plugin - - - io.github.evis - scalafix-maven-plugin_2.13 - - ${project.parent.parent.basedir}/.scalafix.conf - - - - - \ No newline at end of file diff --git a/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/model/GetAllRateLimitPlanResponseDTO.java b/tmail-backend/rate-limiter/rate-limiter-api/src/main/java/com/linagora/tmail/rate/limiter/api/memory/MemoryRateLimitingModule.java similarity index 60% rename from tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/model/GetAllRateLimitPlanResponseDTO.java rename to tmail-backend/rate-limiter/rate-limiter-api/src/main/java/com/linagora/tmail/rate/limiter/api/memory/MemoryRateLimitingModule.java index cf03978ed5..9b6c18f6b1 100644 --- a/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/model/GetAllRateLimitPlanResponseDTO.java +++ b/tmail-backend/rate-limiter/rate-limiter-api/src/main/java/com/linagora/tmail/rate/limiter/api/memory/MemoryRateLimitingModule.java @@ -16,16 +16,24 @@ * more details. * ********************************************************************/ -package com.linagora.tmail.webadmin.model; +package com.linagora.tmail.rate.limiter.api.memory; -import java.util.List; +import org.apache.james.user.api.UsernameChangeTaskStep; -import com.fasterxml.jackson.annotation.JsonValue; +import com.google.inject.AbstractModule; +import com.google.inject.Scopes; +import com.google.inject.multibindings.Multibinder; +import com.linagora.tmail.rate.limiter.api.RateLimitingRepository; +import com.linagora.tmail.rate.limiter.api.RateLimitingUsernameChangeTaskStep; -public record GetAllRateLimitPlanResponseDTO(List list) { +public class MemoryRateLimitingModule extends AbstractModule { @Override - @JsonValue - public List list() { - return list; + protected void configure() { + bind(MemoryRateLimitingRepository.class).in(Scopes.SINGLETON); + bind(RateLimitingRepository.class).to(MemoryRateLimitingRepository.class); + + Multibinder.newSetBinder(binder(), UsernameChangeTaskStep.class) + .addBinding() + .to(RateLimitingUsernameChangeTaskStep.class); } } diff --git a/tmail-backend/rate-limiter/rate-limiter-api/src/main/scala/com/linagora/tmail/rate/limiter/api/CacheRateLimitingPlan.scala b/tmail-backend/rate-limiter/rate-limiter-api/src/main/scala/com/linagora/tmail/rate/limiter/api/CacheRateLimitingPlan.scala deleted file mode 100644 index 173d4049b3..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-api/src/main/scala/com/linagora/tmail/rate/limiter/api/CacheRateLimitingPlan.scala +++ /dev/null @@ -1,81 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api - -import java.lang -import java.time.Duration -import java.util.concurrent.Executor - -import com.github.benmanes.caffeine.cache.{AsyncCacheLoader, AsyncLoadingCache, Caffeine} -import org.apache.james.metrics.api.GaugeRegistry -import org.reactivestreams.Publisher -import reactor.core.publisher.Mono -import reactor.core.scala.publisher.SMono -import reactor.core.scheduler.Schedulers - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.jdk.FutureConverters._ - -class CacheRateLimitingPlan(repository: RateLimitingPlanRepository, expireDuration: Duration, gaugeRegistry: GaugeRegistry, - gaugePrefix: Option[String] = None) extends RateLimitingPlanRepository { - - private val cacheLoaderGet: AsyncCacheLoader[RateLimitingPlanId, RateLimitingPlan] = - (key: RateLimitingPlanId, executor: Executor) => Mono.from(repository.get(key)) - .subscribeOn(Schedulers.fromExecutor(executor)) - .toFuture - - private val gaugePrefixValue: String = gaugePrefix.map(_ + ".").getOrElse("") - - private val cacheGet: AsyncLoadingCache[RateLimitingPlanId, RateLimitingPlan] = { - val loadingCache: AsyncLoadingCache[RateLimitingPlanId, RateLimitingPlan] = Caffeine.newBuilder() - .expireAfterWrite(expireDuration) - .recordStats() - .buildAsync[RateLimitingPlanId, RateLimitingPlan](cacheLoaderGet) - - gaugeRegistry.register(gaugePrefixValue + "rate_limiting_plan.cache.get.hitRate", () => loadingCache.synchronous().stats().hitRate()) - .register(gaugePrefixValue + "rate_limiting_plan.cache.get.missCount", () => loadingCache.synchronous().stats().missCount()) - .register(gaugePrefixValue + "rate_limiting_plan.cache.get.hitCount", () => loadingCache.synchronous().stats().hitCount()) - .register(gaugePrefixValue + "rate_limiting_plan.cache.get.size", () => loadingCache.synchronous().estimatedSize()) - loadingCache - } - - override def create(creationRequest: RateLimitingPlanCreateRequest): Publisher[RateLimitingPlan] = - SMono.fromPublisher(repository.create(creationRequest)) - .doOnNext(_ => invalidCache()) - - override def update(resetRequest: RateLimitingPlanResetRequest): Publisher[Unit] = - SMono.fromPublisher(repository.update(resetRequest)) - .`then`(SMono.fromCallable(() => invalidCache())) - - override def get(id: RateLimitingPlanId): Publisher[RateLimitingPlan] = - SMono.fromFuture(cacheGet.get(id).asScala) - - override def planExists(id: RateLimitingPlanId): Publisher[lang.Boolean] = - SMono.fromFuture(cacheGet.get(id).asScala) - .hasElement - .onErrorResume { - case _: RateLimitingPlanNotFoundException => SMono.just(false) - case error => SMono.error(error) - } - .map(_.booleanValue()) - - override def list(): Publisher[RateLimitingPlan] = repository.list() - - private def invalidCache(): Unit = cacheGet.synchronous().invalidateAll() -} diff --git a/tmail-backend/rate-limiter/rate-limiter-api/src/main/scala/com/linagora/tmail/rate/limiter/api/RateLimitation.scala b/tmail-backend/rate-limiter/rate-limiter-api/src/main/scala/com/linagora/tmail/rate/limiter/api/RateLimitation.scala deleted file mode 100644 index b6ec30b4bf..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-api/src/main/scala/com/linagora/tmail/rate/limiter/api/RateLimitation.scala +++ /dev/null @@ -1,198 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api - -import java.time.Duration -import java.util.UUID - -import com.linagora.tmail.rate.limiter.api.LimitTypes.{COUNT, LimitTypes, SIZE} -import com.linagora.tmail.rate.limiter.api.OperationLimitations.{DELIVERY_LIMITATIONS_NAME, RELAY_LIMITATIONS_NAME, TRANSIT_LIMITATIONS_NAME} -import com.linagora.tmail.rate.limiter.api.OperationLimitationsType.OperationLimitationsType -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanName.RateLimitingPlanName -import eu.timepit.refined -import eu.timepit.refined.api.Refined -import eu.timepit.refined.collection.NonEmpty -import org.apache.james.core.Username -import org.apache.james.rate.limiter.api.AllowedQuantity -import org.apache.james.rate.limiter.api.AllowedQuantity.AllowedQuantity - -object LimitTypes { - val SIZE: String = "size" - val COUNT: String = "count" - - type LimitTypes = Set[LimitType] Refined NonEmpty - - def validate(value: Set[LimitType]): Either[IllegalArgumentException, LimitTypes] = - refined.refineV[NonEmpty](value) match { - case Right(value) => Right(value) - case Left(_) => Left(new IllegalArgumentException("value should not be empty")) - } - - def liftOrThrow(value: Set[LimitType]): LimitTypes = - validate(value) match { - case Right(value) => value - case Left(error) => throw error - } - - def from(limitTypeAndAllowedQuantity: Map[String, Long]): LimitTypes = { - val limitTypes: Set[LimitType] = limitTypeAndAllowedQuantity - .map(map => LimitType.liftOrThrow(map._1, map._2)) - .toSet - liftOrThrow(limitTypes) - } - - def fromMutableMap(limitTypeAndAllowedQuantity: scala.collection.mutable.Map[String, Long]): LimitTypes = - from(limitTypeAndAllowedQuantity.toMap) -} - -object LimitType { - def liftOrThrow(limitTypeName: String, allowedQuantity: Long): LimitType = - from(limitTypeName, allowedQuantity) match { - case Right(value) => value - case Left(error) => throw error - } - - def from(limitTypeName: String, allowedQuantity: Long): Either[IllegalArgumentException, LimitType] = { - limitTypeName match { - case SIZE => AllowedQuantity.validate(allowedQuantity).map(Size) - case COUNT => AllowedQuantity.validate(allowedQuantity).map(Count) - case _ => Left(new IllegalArgumentException(s"`$limitTypeName` is invalid")) - } - } -} - -sealed trait LimitType { - def asString(): String - - def allowedQuantity(): AllowedQuantity -} - -case class Count(quantity: AllowedQuantity) extends LimitType { - override def asString(): String = COUNT - - override def allowedQuantity(): AllowedQuantity = quantity -} - -case class Size(quantity: AllowedQuantity) extends LimitType { - override def asString(): String = SIZE - - override def allowedQuantity(): AllowedQuantity = quantity -} - -case class RateLimitation(name: String, period: Duration, limits: LimitTypes) { - def limitsValue: Set[LimitType] = limits.value -} - -object RateLimitingPlanId { - def generate: RateLimitingPlanId = RateLimitingPlanId(UUID.randomUUID()) - - def parse(string: String): RateLimitingPlanId = RateLimitingPlanId(UUID.fromString(string)) -} - -case class RateLimitingPlanId(value: UUID) { - def serialize(): String = value.toString -} - -object RateLimitingPlanName { - type RateLimitingPlanName = String Refined NonEmpty - - def liftOrThrow(value: String): RateLimitingPlanName = - refined.refineV[NonEmpty](value) match { - case Right(value) => value - case Left(_) => throw new IllegalArgumentException("Rate limiting plan name should not be empty") - } -} - -object OperationLimitationsType { - type OperationLimitationsType = Seq[OperationLimitations] Refined NonEmpty - - def liftOrThrow(value: Seq[OperationLimitations]): OperationLimitationsType = - refined.refineV[NonEmpty](value) match { - case Right(value) => value - case Left(_) => throw new IllegalArgumentException("value should not be empty") - } -} - -object OperationLimitations { - val TRANSIT_LIMITATIONS_NAME: String = "TransitLimitations" - val RELAY_LIMITATIONS_NAME: String = "RelayLimitations" - val DELIVERY_LIMITATIONS_NAME: String = "DeliveryLimitations" - - def liftOrThrow(operationName: String, rateLimitations: Seq[RateLimitation]): OperationLimitations = - from(operationName, rateLimitations) match { - case Right(value) => value - case Left(error) => throw error - } - - def from(operationName: String, rateLimitations: Seq[RateLimitation]): Either[IllegalArgumentException, OperationLimitations] = - operationName match { - case TRANSIT_LIMITATIONS_NAME => Right(TransitLimitations(rateLimitations)) - case RELAY_LIMITATIONS_NAME => Right(RelayLimitations(rateLimitations)) - case DELIVERY_LIMITATIONS_NAME => Right(DeliveryLimitations(rateLimitations)) - case _ => Left(new IllegalArgumentException(s"`$operationName` is invalid")) - } -} - -sealed trait OperationLimitations { - def asString(): String - - def rateLimitations(): Seq[RateLimitation] -} - -case class TransitLimitations(value: Seq[RateLimitation]) extends OperationLimitations { - override def asString(): String = TRANSIT_LIMITATIONS_NAME - - override def rateLimitations(): Seq[RateLimitation] = value -} - -case class RelayLimitations(value: Seq[RateLimitation]) extends OperationLimitations { - override def asString(): String = RELAY_LIMITATIONS_NAME - - override def rateLimitations(): Seq[RateLimitation] = value -} - -case class DeliveryLimitations(value: Seq[RateLimitation]) extends OperationLimitations { - override def asString(): String = DELIVERY_LIMITATIONS_NAME - - override def rateLimitations(): Seq[RateLimitation] = value -} - -object RateLimitingPlan { - def from(createRequest: RateLimitingPlanCreateRequest): RateLimitingPlan = - RateLimitingPlan(id = RateLimitingPlanId.generate, name = createRequest.name, operationLimitations = createRequest.operationLimitations.value) - - def update(origin: RateLimitingPlan, resetRequest: RateLimitingPlanResetRequest): RateLimitingPlan = - origin.copy(name = resetRequest.name, operationLimitations = resetRequest.operationLimitations.value) -} - -case class RateLimitingPlan(id: RateLimitingPlanId, name: RateLimitingPlanName, operationLimitations: Seq[OperationLimitations]) - -case class RateLimitingPlanCreateRequest(name: RateLimitingPlanName, operationLimitations: OperationLimitationsType) { - def operationLimitationsValue: Seq[OperationLimitations] = operationLimitations.value - - def nameValue: String = name.value -} - -case class RateLimitingPlanResetRequest(id: RateLimitingPlanId, name: RateLimitingPlanName, operationLimitations: OperationLimitationsType) { - def operationLimitationsValue: Seq[OperationLimitations] = operationLimitations.value - - def nameValue: String = name.value -} - -case class UsernameToRateLimitingPlanId(username: Username, rateLimitingPlanId: RateLimitingPlanId) diff --git a/tmail-backend/rate-limiter/rate-limiter-api/src/main/scala/com/linagora/tmail/rate/limiter/api/RateLimitingPlanRepository.scala b/tmail-backend/rate-limiter/rate-limiter-api/src/main/scala/com/linagora/tmail/rate/limiter/api/RateLimitingPlanRepository.scala deleted file mode 100644 index b6e07206f4..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-api/src/main/scala/com/linagora/tmail/rate/limiter/api/RateLimitingPlanRepository.scala +++ /dev/null @@ -1,67 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api - -import org.reactivestreams.Publisher -import reactor.core.scala.publisher.{SFlux, SMono} - -import scala.collection.concurrent.Map - -trait RateLimitingPlanRepository { - - def create(creationRequest: RateLimitingPlanCreateRequest): Publisher[RateLimitingPlan] - - def update(resetRequest: RateLimitingPlanResetRequest): Publisher[Unit] - - def get(id: RateLimitingPlanId): Publisher[RateLimitingPlan] - - def planExists(id: RateLimitingPlanId): Publisher[java.lang.Boolean] - - def list(): Publisher[RateLimitingPlan] -} - -case class RateLimitingPlanNotFoundException() extends RuntimeException - -class InMemoryRateLimitingPlanRepository() extends RateLimitingPlanRepository { - - val rateLimitingPlanStore: Map[RateLimitingPlanId, RateLimitingPlan] = scala.collection.concurrent.TrieMap() - - override def create(creationRequest: RateLimitingPlanCreateRequest): Publisher[RateLimitingPlan] = - SMono.fromCallable(() => { - val rateLimitingPlan: RateLimitingPlan = RateLimitingPlan.from(creationRequest) - rateLimitingPlanStore.put(rateLimitingPlan.id, rateLimitingPlan) - rateLimitingPlan - }) - - override def update(resetRequest: RateLimitingPlanResetRequest): Publisher[Unit] = - SMono.justOrEmpty(rateLimitingPlanStore.get(resetRequest.id)) - .switchIfEmpty(SMono.error(new RateLimitingPlanNotFoundException)) - .map(rateLimitingPlan => rateLimitingPlanStore.update(resetRequest.id, RateLimitingPlan.update(rateLimitingPlan, resetRequest))) - - override def get(id: RateLimitingPlanId): Publisher[RateLimitingPlan] = - SMono.justOrEmpty(rateLimitingPlanStore.get(id)) - .switchIfEmpty(SMono.error(new RateLimitingPlanNotFoundException)) - - - override def list(): Publisher[RateLimitingPlan] = - SFlux.fromIterable(rateLimitingPlanStore) - .map(_._2) - - override def planExists(id: RateLimitingPlanId): Publisher[java.lang.Boolean] = SMono.just(rateLimitingPlanStore.contains(id)) -} \ No newline at end of file diff --git a/tmail-backend/rate-limiter/rate-limiter-api/src/main/scala/com/linagora/tmail/rate/limiter/api/RateLimitingPlanUserRepository.scala b/tmail-backend/rate-limiter/rate-limiter-api/src/main/scala/com/linagora/tmail/rate/limiter/api/RateLimitingPlanUserRepository.scala deleted file mode 100644 index f2170eac50..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-api/src/main/scala/com/linagora/tmail/rate/limiter/api/RateLimitingPlanUserRepository.scala +++ /dev/null @@ -1,32 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api - -import org.apache.james.core.Username -import org.reactivestreams.Publisher - -trait RateLimitingPlanUserRepository { - def applyPlan(username: Username, planId: RateLimitingPlanId): Publisher[Unit] - - def revokePlan(username: Username): Publisher[Unit] - - def listUsers(planId: RateLimitingPlanId): Publisher[Username] - - def getPlanByUser(username: Username): Publisher[RateLimitingPlanId] -} diff --git a/tmail-backend/rate-limiter/rate-limiter-api/src/main/scala/com/linagora/tmail/rate/limiter/api/RateLimitingPlanUsernameChangeTaskStep.scala b/tmail-backend/rate-limiter/rate-limiter-api/src/main/scala/com/linagora/tmail/rate/limiter/api/RateLimitingPlanUsernameChangeTaskStep.scala deleted file mode 100644 index 851626f8c9..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-api/src/main/scala/com/linagora/tmail/rate/limiter/api/RateLimitingPlanUsernameChangeTaskStep.scala +++ /dev/null @@ -1,48 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api - -import jakarta.inject.Inject -import org.apache.james.core.Username -import org.apache.james.user.api.UsernameChangeTaskStep -import org.apache.james.user.api.UsernameChangeTaskStep.StepName -import org.reactivestreams.Publisher -import reactor.core.scala.publisher.SMono - -class RateLimitingPlanUsernameChangeTaskStep @Inject() (val repository: RateLimitingPlanUserRepository) extends UsernameChangeTaskStep { - override def name(): StepName = new StepName("RateLimitingPlanUsernameChangeTaskStep") - - override def priority(): Int = 7 - - override def changeUsername(oldUsername: Username, newUsername: Username): Publisher[Void] = { - SMono(repository.getPlanByUser(newUsername)) - .onErrorResume { - case _: RateLimitingPlanNotFoundException => migratePlan(oldUsername, newUsername) - } - .`then`(SMono(repository.revokePlan(oldUsername))) - .`then`() - } - - private def migratePlan(oldUsername: Username, newUsername: Username) = - SMono(repository.getPlanByUser(oldUsername)) - .onErrorResume { - case _: RateLimitingPlanNotFoundException => SMono.empty - } - .flatMap(planId => SMono(repository.applyPlan(newUsername, planId))) -} diff --git a/tmail-backend/rate-limiter/rate-limiter-api/src/main/scala/com/linagora/tmail/rate/limiter/api/memory/MemoryRateLimitingModule.scala b/tmail-backend/rate-limiter/rate-limiter-api/src/main/scala/com/linagora/tmail/rate/limiter/api/memory/MemoryRateLimitingModule.scala deleted file mode 100644 index dd70389cbb..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-api/src/main/scala/com/linagora/tmail/rate/limiter/api/memory/MemoryRateLimitingModule.scala +++ /dev/null @@ -1,46 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.memory - -import com.google.inject.multibindings.Multibinder -import com.google.inject.{AbstractModule, Scopes} -import com.linagora.tmail.rate.limiter.api.{InMemoryRateLimitingPlanRepository, RateLimitingPlanRepository, RateLimitingPlanUserRepository, RateLimitingPlanUsernameChangeTaskStep, RateLimitingRepository, RateLimitingUsernameChangeTaskStep} -import org.apache.james.user.api.UsernameChangeTaskStep - -class MemoryRateLimitingModule() extends AbstractModule { - - override def configure(): Unit = { - bind(classOf[MemoryRateLimitingPlanUserRepository]).in(Scopes.SINGLETON) - bind(classOf[InMemoryRateLimitingPlanRepository]).in(Scopes.SINGLETON) - - bind(classOf[RateLimitingPlanUserRepository]).to(classOf[MemoryRateLimitingPlanUserRepository]) - bind(classOf[RateLimitingPlanRepository]).to(classOf[InMemoryRateLimitingPlanRepository]) - - Multibinder.newSetBinder(binder(), classOf[UsernameChangeTaskStep]) - .addBinding() - .to(classOf[RateLimitingPlanUsernameChangeTaskStep]) - - bind(classOf[MemoryRateLimitingRepository]).in(Scopes.SINGLETON) - bind(classOf[RateLimitingRepository]).to(classOf[MemoryRateLimitingRepository]) - - Multibinder.newSetBinder(binder(), classOf[UsernameChangeTaskStep]) - .addBinding() - .to(classOf[RateLimitingUsernameChangeTaskStep]) - } -} diff --git a/tmail-backend/rate-limiter/rate-limiter-api/src/main/scala/com/linagora/tmail/rate/limiter/api/memory/MemoryRateLimitingPlanUserRepository.scala b/tmail-backend/rate-limiter/rate-limiter-api/src/main/scala/com/linagora/tmail/rate/limiter/api/memory/MemoryRateLimitingPlanUserRepository.scala deleted file mode 100644 index 2c80e0182d..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-api/src/main/scala/com/linagora/tmail/rate/limiter/api/memory/MemoryRateLimitingPlanUserRepository.scala +++ /dev/null @@ -1,52 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.memory - -import com.google.common.base.Preconditions -import com.linagora.tmail.rate.limiter.api.{RateLimitingPlanId, RateLimitingPlanNotFoundException, RateLimitingPlanUserRepository} -import org.apache.james.core.Username -import reactor.core.scala.publisher.{SFlux, SMono} - -import scala.collection.concurrent.Map - -class MemoryRateLimitingPlanUserRepository extends RateLimitingPlanUserRepository { - val table: Map[Username, RateLimitingPlanId] = scala.collection.concurrent.TrieMap() - - override def applyPlan(username: Username, planId: RateLimitingPlanId): SMono[Unit] = { - Preconditions.checkNotNull(username) - Preconditions.checkNotNull(planId) - SMono.fromCallable(() => table.put(username, planId)).`then`() - } - - override def revokePlan(username: Username): SMono[Unit] = { - Preconditions.checkNotNull(username) - SMono.fromCallable(() => table.remove(username)).`then`() - } - - override def listUsers(planId: RateLimitingPlanId): SFlux[Username] = { - Preconditions.checkNotNull(planId) - SFlux.fromIterable(table.filter(usernameToPlanId => usernameToPlanId._2.equals(planId)).keys) - } - - override def getPlanByUser(username: Username): SMono[RateLimitingPlanId] = { - Preconditions.checkNotNull(username) - SMono.justOrEmpty(table.get(username)) - .switchIfEmpty(SMono.error(RateLimitingPlanNotFoundException())) - } -} diff --git a/tmail-backend/rate-limiter/rate-limiter-api/src/test/scala/com/linagora/tmail/rate/limiter/api/CacheInMemoryRateLimitingPlanRepositoryTest.scala b/tmail-backend/rate-limiter/rate-limiter-api/src/test/scala/com/linagora/tmail/rate/limiter/api/CacheInMemoryRateLimitingPlanRepositoryTest.scala deleted file mode 100644 index 8013bd3610..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-api/src/test/scala/com/linagora/tmail/rate/limiter/api/CacheInMemoryRateLimitingPlanRepositoryTest.scala +++ /dev/null @@ -1,36 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api - -import java.time.Duration - -import org.apache.james.metrics.api.NoopGaugeRegistry -import org.junit.jupiter.api.BeforeEach - -class CacheInMemoryRateLimitingPlanRepositoryTest extends RateLimitingPlanRepositoryContract { - var repository: RateLimitingPlanRepository = _ - - override def testee: RateLimitingPlanRepository = repository - - @BeforeEach - def beforeEach(): Unit = { - val inMemoryRepository: RateLimitingPlanRepository = new InMemoryRateLimitingPlanRepository() - repository = new CacheRateLimitingPlan(inMemoryRepository, Duration.ofMinutes(2), new NoopGaugeRegistry) - } -} \ No newline at end of file diff --git a/tmail-backend/rate-limiter/rate-limiter-api/src/test/scala/com/linagora/tmail/rate/limiter/api/RateLimitingPlanRepositoryContract.scala b/tmail-backend/rate-limiter/rate-limiter-api/src/test/scala/com/linagora/tmail/rate/limiter/api/RateLimitingPlanRepositoryContract.scala deleted file mode 100644 index a07689682f..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-api/src/test/scala/com/linagora/tmail/rate/limiter/api/RateLimitingPlanRepositoryContract.scala +++ /dev/null @@ -1,227 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api - -import java.time.Duration - -import com.linagora.tmail.rate.limiter.api.LimitTypes.LimitTypes -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanRepositoryContract.{CREATION_REQUEST, CREATION_REQUEST_WITH_MULTI_OPERATIONS, RESET_REQUEST} -import eu.timepit.refined.auto._ -import org.apache.james.rate.limiter.api.AllowedQuantity -import org.assertj.core.api.Assertions.{assertThat, assertThatCode, assertThatThrownBy} -import org.assertj.core.api.SoftAssertions -import org.junit.jupiter.api.{BeforeEach, Test} -import reactor.core.scala.publisher.{SFlux, SMono} - -import scala.jdk.CollectionConverters._ - -case class TestPJ(name: String, limits: LimitTypes) - -object RateLimitingPlanRepositoryContract { - def getAllowedQuantity(value: Long): AllowedQuantity.AllowedQuantity = AllowedQuantity.validate(value).toOption.get - - val COUNT: Count = Count(getAllowedQuantity(1)) - val SIZE: Size = Size(getAllowedQuantity(10000)) - val LIMIT_TYPES: LimitTypes = LimitTypes.liftOrThrow(Set(COUNT, SIZE)) - val RATE_LIMITATION: RateLimitation = RateLimitation("name1", Duration.ofMinutes(1), LIMIT_TYPES) - val TRANSIT_LIMITS: TransitLimitations = TransitLimitations(Seq(RATE_LIMITATION)) - val RELAY_LIMITS: RelayLimitations = RelayLimitations(Seq(RATE_LIMITATION)) - - val CREATION_REQUEST: RateLimitingPlanCreateRequest = RateLimitingPlanCreateRequest( - name = "name1", - operationLimitations = OperationLimitationsType.liftOrThrow(Seq(TRANSIT_LIMITS))) - val RESET_REQUEST: RateLimitingPlanResetRequest = RateLimitingPlanResetRequest( - id = RateLimitingPlanId.generate, - name = "new name", - operationLimitations = OperationLimitationsType.liftOrThrow(Seq(TRANSIT_LIMITS))) - - val CREATION_REQUEST_WITH_MULTI_OPERATIONS: RateLimitingPlanCreateRequest = RateLimitingPlanCreateRequest( - name = "complex_plan", - operationLimitations = OperationLimitationsType.liftOrThrow(Seq( - TransitLimitations(Seq( - RateLimitation(name = "limit1", - period = Duration.ofMinutes(1), - limits = LimitTypes.liftOrThrow(Set(Count(getAllowedQuantity(1)), Size(getAllowedQuantity(10000))))), - RateLimitation(name = "limit2", - period = Duration.ofMinutes(2), - limits = LimitTypes.liftOrThrow(Set(Count(getAllowedQuantity(2)), Size(getAllowedQuantity(20000))))))), - RelayLimitations(Seq( - RateLimitation(name = "limit3", - period = Duration.ofMinutes(3), - limits = LimitTypes.liftOrThrow(Set(Count(getAllowedQuantity(3)), Size(getAllowedQuantity(30000))))), - RateLimitation(name = "limit4", - period = Duration.ofMinutes(4), - limits = LimitTypes.liftOrThrow(Set(Count(getAllowedQuantity(4)), Size(getAllowedQuantity(40000)))))))))) -} - -trait RateLimitingPlanRepositoryContract { - def testee: RateLimitingPlanRepository - - @Test - def createShouldReturnRateLimitingPlan(): Unit = { - val rateLimitingPlan: RateLimitingPlan = SMono.fromPublisher(testee.create(CREATION_REQUEST)) - .block() - - SoftAssertions.assertSoftly(softly => { - softly.assertThat(rateLimitingPlan.id).isNotNull - softly.assertThat(rateLimitingPlan.operationLimitations.asJava) - .containsExactlyInAnyOrderElementsOf(CREATION_REQUEST.operationLimitations.value.asJava) - softly.assertThat(rateLimitingPlan.name).isEqualTo(CREATION_REQUEST.name) - }) - } - - @Test - def createShouldStoreEntry(): Unit = { - val rateLimitingPlan: RateLimitingPlan = SMono.fromPublisher(testee.create(CREATION_REQUEST)) - .block() - - assertThat(SMono.fromPublisher(testee.get(rateLimitingPlan.id)) - .block()) - .isEqualTo(rateLimitingPlan) - } - - @Test - def createShouldReturnDifferentEntry(): Unit = { - val rateLimitingPlan: RateLimitingPlan = SMono.fromPublisher(testee.create(CREATION_REQUEST)) - .block() - - assertThat(SMono.fromPublisher(testee.create(CREATION_REQUEST)) - .block()) - .isNotEqualTo(rateLimitingPlan) - } - - @Test - def createShouldWorkWhenHaveSeveralOperationLimitations(): Unit = { - val rateLimitingPlan: RateLimitingPlan = SMono.fromPublisher(testee.create(CREATION_REQUEST_WITH_MULTI_OPERATIONS)) - .block() - - assertThat(SFlux.fromPublisher(testee.list()).collectSeq().block().asJava) - .containsExactlyInAnyOrder(rateLimitingPlan) - } - - @Test - def updateShouldThrowWhenIdNotFound(): Unit = { - assertThatThrownBy(() => SMono.fromPublisher(testee.update(RESET_REQUEST)).block()) - .isInstanceOf(classOf[RateLimitingPlanNotFoundException]) - } - - @Test - def updateShouldNotThrowWhenIdExists(): Unit = { - val rateLimitingPlan: RateLimitingPlan = SMono.fromPublisher(testee.create(CREATION_REQUEST)) - .block() - - assertThatCode(() => SMono.fromPublisher(testee.update(RESET_REQUEST.copy(id = rateLimitingPlan.id))).block()) - .doesNotThrowAnyException() - } - - @Test - def updateShouldModifyEntry(): Unit = { - val rateLimitingPlan: RateLimitingPlan = SMono.fromPublisher(testee.create(CREATION_REQUEST)) - .block() - - SMono.fromPublisher(testee.update(RESET_REQUEST.copy(id = rateLimitingPlan.id))).block() - - assertThat(SMono.fromPublisher(testee.get(rateLimitingPlan.id)).block()) - .isEqualTo(RateLimitingPlan( - id = rateLimitingPlan.id, - name = RESET_REQUEST.name, - operationLimitations = RESET_REQUEST.operationLimitations)) - } - - @Test - def updateShouldNotModifyAnotherEntry(): Unit = { - val rateLimitingPlan: RateLimitingPlan = SMono.fromPublisher(testee.create(CREATION_REQUEST)) - .block() - val rateLimitingPlan2: RateLimitingPlan = SMono.fromPublisher(testee.create(CREATION_REQUEST)) - .block() - - SMono.fromPublisher(testee.update(RESET_REQUEST.copy(id = rateLimitingPlan.id))).block() - - assertThat(SMono.fromPublisher(testee.get(rateLimitingPlan2.id)) - .block()) - .isEqualTo(rateLimitingPlan2) - } - - @Test - def getShouldThrowWhenIdNotFound(): Unit = { - assertThatThrownBy(() => SMono.fromPublisher(testee.get(RateLimitingPlanId.generate)).block()) - .isInstanceOf(classOf[RateLimitingPlanNotFoundException]) - } - - @Test - def getShouldReturnEntryWhenIdExists(): Unit = { - val rateLimitingPlan: RateLimitingPlan = SMono.fromPublisher(testee.create(CREATION_REQUEST)) - .block() - - assertThat(SMono.fromPublisher(testee.get(rateLimitingPlan.id)).block()) - .isEqualTo(rateLimitingPlan) - } - - @Test - def planExistsShouldReturnFalseByDefault(): Unit = { - assertThat(SMono.fromPublisher(testee.planExists(RateLimitingPlanId.generate)).block()) - .isEqualTo(false) - } - - @Test - def planExistsShouldReturnTrueWhenPlanExists(): Unit = { - val rateLimitingPlan: RateLimitingPlan = SMono.fromPublisher(testee.create(CREATION_REQUEST)) - .block() - - assertThat(SMono.fromPublisher(testee.planExists(rateLimitingPlan.id)).block()) - .isEqualTo(true) - } - - @Test - def listShouldReturnEmptyByDefault(): Unit = { - assertThat(SFlux.fromPublisher(testee.list()).collectSeq().block().asJava) - .isEmpty() - } - - @Test - def listShouldReturnStoredEntriesWhenHasSingleElement(): Unit = { - val rateLimitingPlan: RateLimitingPlan = SMono.fromPublisher(testee.create(CREATION_REQUEST)) - .block() - - assertThat(SFlux.fromPublisher(testee.list()).collectSeq().block().asJava) - .containsExactlyInAnyOrder(rateLimitingPlan) - } - - @Test - def listShouldReturnStoredEntriesWhenHasSeveralElement(): Unit = { - val rateLimitingPlan: RateLimitingPlan = SMono.fromPublisher(testee.create(CREATION_REQUEST)) - .block() - val rateLimitingPlan2: RateLimitingPlan = SMono.fromPublisher(testee.create(CREATION_REQUEST_WITH_MULTI_OPERATIONS)) - .block() - - assertThat(SFlux.fromPublisher(testee.list()).collectSeq().block().asJava) - .containsExactlyInAnyOrder(rateLimitingPlan, rateLimitingPlan2) - } -} - -class InMemoryRateLimitingPlanRepositoryTest extends RateLimitingPlanRepositoryContract { - var inMemoryRateLimitationPlanRepository: InMemoryRateLimitingPlanRepository = _ - - override def testee: RateLimitingPlanRepository = inMemoryRateLimitationPlanRepository - - @BeforeEach - def beforeEach(): Unit = { - inMemoryRateLimitationPlanRepository = new InMemoryRateLimitingPlanRepository(); - } -} \ No newline at end of file diff --git a/tmail-backend/rate-limiter/rate-limiter-api/src/test/scala/com/linagora/tmail/rate/limiter/api/RateLimitingPlanUserRepositoryContract.scala b/tmail-backend/rate-limiter/rate-limiter-api/src/test/scala/com/linagora/tmail/rate/limiter/api/RateLimitingPlanUserRepositoryContract.scala deleted file mode 100644 index d431a75169..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-api/src/test/scala/com/linagora/tmail/rate/limiter/api/RateLimitingPlanUserRepositoryContract.scala +++ /dev/null @@ -1,147 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api - -import java.util.UUID - -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanUserRepositoryContract.{ALICE, BOB, PLAN_ID_1, PLAN_ID_2} -import org.apache.james.core.Username -import org.assertj.core.api.Assertions.{assertThat, assertThatThrownBy} -import org.assertj.core.api.SoftAssertions -import org.junit.jupiter.api.Test -import reactor.core.scala.publisher.{SFlux, SMono} - -import scala.jdk.CollectionConverters._ - -object RateLimitingPlanUserRepositoryContract { - val BOB: Username = Username.of("BOB@domain1.tld") - val ALICE: Username = Username.of("ALICE@domain2.tld") - val PLAN_ID_1: RateLimitingPlanId = RateLimitingPlanId(UUID.randomUUID()) - val PLAN_ID_2: RateLimitingPlanId = RateLimitingPlanId(UUID.randomUUID()) -} - -trait RateLimitingPlanUserRepositoryContract { - def testee: RateLimitingPlanUserRepository - - @Test - def applyPlanShouldSucceed(): Unit = { - SMono.fromPublisher(testee.applyPlan(BOB, PLAN_ID_1)).block() - - assertThat(SMono.fromPublisher(testee.getPlanByUser(BOB)).block()).isEqualTo(PLAN_ID_1) - } - - @Test - def applyPlanWhenManyUsersHaveSamePlanShouldSucceed(): Unit = { - SMono.fromPublisher(testee.applyPlan(BOB, PLAN_ID_1)).block() - SMono.fromPublisher(testee.applyPlan(ALICE, PLAN_ID_1)).block() - - SoftAssertions.assertSoftly(softly => { - softly.assertThat(SMono.fromPublisher(testee.getPlanByUser(BOB)).block().value).isEqualTo(PLAN_ID_1.value) - softly.assertThat(SMono.fromPublisher(testee.getPlanByUser(ALICE)).block().value).isEqualTo(PLAN_ID_1.value) - }) - } - - @Test - def applyPlanShouldOverridePreviousPlan(): Unit = { - SMono.fromPublisher(testee.applyPlan(BOB, PLAN_ID_1)).block() - SMono.fromPublisher(testee.applyPlan(BOB, PLAN_ID_2)).block() - - assertThat(SMono.fromPublisher(testee.getPlanByUser(BOB)).block()).isEqualTo(PLAN_ID_2) - } - - @Test - def applyPlanShouldThrowWhenUsernameIsNull(): Unit = { - assertThatThrownBy(() => SMono.fromPublisher(testee.applyPlan(null, PLAN_ID_1)).block()) - .isInstanceOf(classOf[NullPointerException]) - } - - @Test - def applyPlanShouldThrowWhenPlanIdIsNull(): Unit = { - assertThatThrownBy(() => SMono.fromPublisher(testee.applyPlan(BOB, null)).block()) - .isInstanceOf(classOf[NullPointerException]) - } - - @Test - def revokePlanShouldCleanTheRecord(): Unit = { - SMono.fromPublisher(testee.applyPlan(BOB, PLAN_ID_1)).block() - SMono.fromPublisher(testee.revokePlan(BOB)).block() - - assertThatThrownBy(() => SMono.fromPublisher(testee.getPlanByUser(BOB)).block()) - .isInstanceOf(classOf[RateLimitingPlanNotFoundException]) - } - - @Test - def insertSettingsAfterDeletingSettingsShouldWorkWell(): Unit = { - // This test simulates a scenario where the user record exists but has no settings yet. - SMono.fromPublisher(testee.applyPlan(BOB, PLAN_ID_1)).block() - SMono.fromPublisher(testee.revokePlan(BOB)).block() - - SMono.fromPublisher(testee.applyPlan(BOB, PLAN_ID_2)).block() - - assertThat(SMono.fromPublisher(testee.getPlanByUser(BOB)).block()).isEqualTo(PLAN_ID_2) - } - - @Test - def revokePlanShouldThrowWhenUsernameIsNull(): Unit = { - assertThatThrownBy(() => SMono.fromPublisher(testee.revokePlan(null)).block()) - .isInstanceOf(classOf[NullPointerException]) - } - - @Test - def getPlanByUserShouldSucceed(): Unit = { - SMono.fromPublisher(testee.applyPlan(BOB, PLAN_ID_1)).block() - SMono.fromPublisher(testee.applyPlan(ALICE, PLAN_ID_2)).block() - - SoftAssertions.assertSoftly(softly => { - softly.assertThat(SMono.fromPublisher(testee.getPlanByUser(BOB)).block().value).isEqualTo(PLAN_ID_1.value) - softly.assertThat(SMono.fromPublisher(testee.getPlanByUser(ALICE)).block().value).isEqualTo(PLAN_ID_2.value) - }) - } - - @Test - def getPlanByUserWhenNotFoundShouldThrowException(): Unit = { - assertThatThrownBy(() => SMono.fromPublisher(testee.getPlanByUser(BOB)).block()) - .isInstanceOf(classOf[RateLimitingPlanNotFoundException]) - } - - @Test - def getPlanByUserShouldThrowWhenUsernameIsNull(): Unit = { - assertThatThrownBy(() => SMono.fromPublisher(testee.getPlanByUser(null)).block()) - .isInstanceOf(classOf[NullPointerException]) - } - - @Test - def listUserShouldSucceed(): Unit = { - SMono.fromPublisher(testee.applyPlan(BOB, PLAN_ID_1)).block() - SMono.fromPublisher(testee.applyPlan(ALICE, PLAN_ID_1)).block() - - assertThat(SFlux.fromPublisher(testee.listUsers(PLAN_ID_1)).collectSeq().block().asJava).containsExactlyInAnyOrder(BOB, ALICE) - } - - @Test - def listUserShouldReturnEmptyByDefault(): Unit = { - assertThat(SFlux.fromPublisher(testee.listUsers(PLAN_ID_1)).collectSeq().block().asJava).isEmpty() - } - - @Test - def listUserShouldThrowWhenPlanIdIsNull(): Unit = { - assertThatThrownBy(() => SFlux.fromPublisher(testee.listUsers(null)).collectSeq().block()) - .isInstanceOf(classOf[NullPointerException]) - } -} diff --git a/tmail-backend/rate-limiter/rate-limiter-api/src/test/scala/com/linagora/tmail/rate/limiter/api/RateLimitingPlanUsernameChangeTaskStepTest.scala b/tmail-backend/rate-limiter/rate-limiter-api/src/test/scala/com/linagora/tmail/rate/limiter/api/RateLimitingPlanUsernameChangeTaskStepTest.scala deleted file mode 100644 index 11e512d4ef..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-api/src/test/scala/com/linagora/tmail/rate/limiter/api/RateLimitingPlanUsernameChangeTaskStepTest.scala +++ /dev/null @@ -1,118 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api - -import java.time.Duration - -import com.linagora.tmail.rate.limiter.api.LimitTypes.LimitTypes -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanUserRepositoryContract.{ALICE, BOB} -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanUsernameChangeTaskStepTest.{CREATION_REQUEST, CREATION_REQUEST_2} -import com.linagora.tmail.rate.limiter.api.memory.MemoryRateLimitingPlanUserRepository -import eu.timepit.refined.auto._ -import org.apache.james.rate.limiter.api.AllowedQuantity -import org.assertj.core.api.Assertions.{assertThat, assertThatThrownBy} -import org.junit.jupiter.api.{BeforeEach, Test} -import reactor.core.scala.publisher.SMono - -object RateLimitingPlanUsernameChangeTaskStepTest { - val COUNT: Count = Count(AllowedQuantity.validate(1).toOption.get) - val SIZE: Size = Size(AllowedQuantity.validate(10000).toOption.get) - val LIMIT_TYPES: LimitTypes = LimitTypes.liftOrThrow(Set(COUNT, SIZE)) - val RATE_LIMITATION: RateLimitation = RateLimitation("name1", Duration.ofMinutes(1), LIMIT_TYPES) - val TRANSIT_LIMITS: TransitLimitations = TransitLimitations(Seq(RATE_LIMITATION)) - val RELAY_LIMITS: RelayLimitations = RelayLimitations(Seq(RATE_LIMITATION)) - val CREATION_REQUEST: RateLimitingPlanCreateRequest = RateLimitingPlanCreateRequest( - name = "plan1", - operationLimitations = OperationLimitationsType.liftOrThrow(Seq(TRANSIT_LIMITS))) - val CREATION_REQUEST_2: RateLimitingPlanCreateRequest = RateLimitingPlanCreateRequest( - name = "plan2", - operationLimitations = OperationLimitationsType.liftOrThrow(Seq(TRANSIT_LIMITS))) -} - -class RateLimitingPlanUsernameChangeTaskStepTest { - - var rateLimitingPlanRepository: RateLimitingPlanRepository = _ - var rateLimitingPlanUserRepository: RateLimitingPlanUserRepository = _ - var testee: RateLimitingPlanUsernameChangeTaskStep = _ - - @BeforeEach - def beforeEach(): Unit = { - rateLimitingPlanRepository = new InMemoryRateLimitingPlanRepository() - rateLimitingPlanUserRepository = new MemoryRateLimitingPlanUserRepository() - testee = new RateLimitingPlanUsernameChangeTaskStep(rateLimitingPlanUserRepository) - } - - @Test - def shouldMigrateRateLimitingPlans(): Unit = { - val planId: RateLimitingPlanId = SMono.fromPublisher(rateLimitingPlanRepository.create(CREATION_REQUEST)) - .map(_.id) - .block() - SMono.fromPublisher(rateLimitingPlanUserRepository.applyPlan(ALICE, planId)).block() - - SMono.fromPublisher(testee.changeUsername(ALICE, BOB)).block() - - assertThat(SMono.fromPublisher(rateLimitingPlanUserRepository.getPlanByUser(BOB)).block()) - .isEqualTo(planId) - } - - @Test - def shouldRevokeOldUserRateLimitingPlans(): Unit = { - val planId: RateLimitingPlanId = SMono.fromPublisher(rateLimitingPlanRepository.create(CREATION_REQUEST)) - .map(_.id) - .block() - SMono.fromPublisher(rateLimitingPlanUserRepository.applyPlan(ALICE, planId)).block() - - SMono.fromPublisher(testee.changeUsername(ALICE, BOB)).block() - - assertThatThrownBy(() => SMono.fromPublisher(rateLimitingPlanUserRepository.getPlanByUser(ALICE)).block()) - .isInstanceOf(classOf[RateLimitingPlanNotFoundException]) - } - - @Test - def shouldNotOverrideNewUserRateLimitingPlans(): Unit = { - val planId: RateLimitingPlanId = SMono.fromPublisher(rateLimitingPlanRepository.create(CREATION_REQUEST)) - .map(_.id) - .block() - SMono.fromPublisher(rateLimitingPlanUserRepository.applyPlan(ALICE, planId)).block() - - val planId2: RateLimitingPlanId = SMono.fromPublisher(rateLimitingPlanRepository.create(CREATION_REQUEST_2)) - .map(_.id) - .block() - SMono.fromPublisher(rateLimitingPlanUserRepository.applyPlan(BOB, planId2)).block() - - SMono.fromPublisher(testee.changeUsername(ALICE, BOB)).block() - - assertThat(SMono.fromPublisher(rateLimitingPlanUserRepository.getPlanByUser(BOB)).block()) - .isEqualTo(planId2) - assertThatThrownBy(() => SMono.fromPublisher(rateLimitingPlanUserRepository.getPlanByUser(ALICE)).block()) - .isInstanceOf(classOf[RateLimitingPlanNotFoundException]) - } - - @Test - def migrateShouldSucceedWhenOldUserHasNoPlan(): Unit = { - SMono.fromPublisher(testee.changeUsername(ALICE, BOB)).block() - - assertThatThrownBy(() => SMono.fromPublisher(rateLimitingPlanUserRepository.getPlanByUser(ALICE)).block()) - .isInstanceOf(classOf[RateLimitingPlanNotFoundException]) - - assertThatThrownBy(() => SMono.fromPublisher(rateLimitingPlanUserRepository.getPlanByUser(BOB)).block()) - .isInstanceOf(classOf[RateLimitingPlanNotFoundException]) - } - -} diff --git a/tmail-backend/rate-limiter/rate-limiter-api/src/test/scala/com/linagora/tmail/rate/limiter/api/memory/MemoryRateLimitingPlanUserRepositoryTest.scala b/tmail-backend/rate-limiter/rate-limiter-api/src/test/scala/com/linagora/tmail/rate/limiter/api/memory/MemoryRateLimitingPlanUserRepositoryTest.scala deleted file mode 100644 index 5489e3a3b9..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-api/src/test/scala/com/linagora/tmail/rate/limiter/api/memory/MemoryRateLimitingPlanUserRepositoryTest.scala +++ /dev/null @@ -1,27 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.memory - -import com.linagora.tmail.rate.limiter.api.{RateLimitingPlanUserRepository, RateLimitingPlanUserRepositoryContract} - -class MemoryRateLimitingPlanUserRepositoryTest extends RateLimitingPlanUserRepositoryContract { - val repository: RateLimitingPlanUserRepository = new MemoryRateLimitingPlanUserRepository - - override def testee: RateLimitingPlanUserRepository = repository -} diff --git a/tmail-backend/rate-limiter/rate-limiter-cassandra/pom.xml b/tmail-backend/rate-limiter/rate-limiter-cassandra/pom.xml index b7c204564b..069588ff39 100644 --- a/tmail-backend/rate-limiter/rate-limiter-cassandra/pom.xml +++ b/tmail-backend/rate-limiter/rate-limiter-cassandra/pom.xml @@ -67,19 +67,4 @@ guice - - - - net.alchim31.maven - scala-maven-plugin - - - io.github.evis - scalafix-maven-plugin_2.13 - - ${project.parent.parent.basedir}/.scalafix.conf - - - - \ No newline at end of file diff --git a/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/model/RateLimitationDTO.java b/tmail-backend/rate-limiter/rate-limiter-cassandra/src/main/java/com/linagora/tmail/rate/limiter/api/cassandra/module/CassandraRateLimitingModule.java similarity index 56% rename from tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/model/RateLimitationDTO.java rename to tmail-backend/rate-limiter/rate-limiter-cassandra/src/main/java/com/linagora/tmail/rate/limiter/api/cassandra/module/CassandraRateLimitingModule.java index 3b689549e6..c461069b2b 100644 --- a/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/model/RateLimitationDTO.java +++ b/tmail-backend/rate-limiter/rate-limiter-cassandra/src/main/java/com/linagora/tmail/rate/limiter/api/cassandra/module/CassandraRateLimitingModule.java @@ -16,23 +16,25 @@ * more details. * ********************************************************************/ -package com.linagora.tmail.webadmin.model; +package com.linagora.tmail.rate.limiter.api.cassandra.module; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.james.user.api.UsernameChangeTaskStep; -public record RateLimitationDTO(String name, - Long periodInSeconds, - Long count, - Long size) { - @JsonCreator - public RateLimitationDTO(@JsonProperty(value = "name", required = true) String name, - @JsonProperty(value = "periodInSeconds", required = true) Long periodInSeconds, - @JsonProperty(value = "count", required = true) Long count, - @JsonProperty(value = "size", required = true) Long size) { - this.name = name; - this.periodInSeconds = periodInSeconds; - this.count = count; - this.size = size; +import com.google.inject.AbstractModule; +import com.google.inject.Scopes; +import com.google.inject.multibindings.Multibinder; +import com.linagora.tmail.rate.limiter.api.RateLimitingRepository; +import com.linagora.tmail.rate.limiter.api.RateLimitingUsernameChangeTaskStep; +import com.linagora.tmail.rate.limiter.api.cassandra.CassandraRateLimitingRepository; + +public class CassandraRateLimitingModule extends AbstractModule { + @Override + protected void configure() { + bind(CassandraRateLimitingRepository.class).in(Scopes.SINGLETON); + bind(RateLimitingRepository.class).to(CassandraRateLimitingRepository.class); + + Multibinder.newSetBinder(binder(), UsernameChangeTaskStep.class) + .addBinding() + .to(RateLimitingUsernameChangeTaskStep.class); } } diff --git a/tmail-backend/rate-limiter/rate-limiter-cassandra/src/main/scala/com/linagora/tmail/rate/limiter/api/cassandra/CassandraRateLimitingPlanRepository.scala b/tmail-backend/rate-limiter/rate-limiter-cassandra/src/main/scala/com/linagora/tmail/rate/limiter/api/cassandra/CassandraRateLimitingPlanRepository.scala deleted file mode 100644 index 8dc124ce6c..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-cassandra/src/main/scala/com/linagora/tmail/rate/limiter/api/cassandra/CassandraRateLimitingPlanRepository.scala +++ /dev/null @@ -1,72 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.cassandra - -import com.google.common.base.Preconditions -import com.linagora.tmail.rate.limiter.api.cassandra.dao.{CassandraRateLimitPlanDAO, RateLimitingPlanEntry} -import com.linagora.tmail.rate.limiter.api.{RateLimitingPlan, RateLimitingPlanCreateRequest, RateLimitingPlanId, RateLimitingPlanName, RateLimitingPlanNotFoundException, RateLimitingPlanRepository, RateLimitingPlanResetRequest} -import jakarta.inject.Inject -import org.apache.james.util.ReactorUtils.DEFAULT_CONCURRENCY -import org.reactivestreams.Publisher -import reactor.core.scala.publisher.{SFlux, SMono} - -class CassandraRateLimitingPlanRepository @Inject()(cassandraRateLimitPlanDAO: CassandraRateLimitPlanDAO) extends RateLimitingPlanRepository { - - override def create(creationRequest: RateLimitingPlanCreateRequest): Publisher[RateLimitingPlan] = - SMono.fromCallable(() => RateLimitingPlanId.generate) - .flatMap(planId => create(planId, RateLimitingPlanEntry.from(planId, creationRequest))) - .flatMap(planId => SMono.fromPublisher(get(planId))) - - private def create(planId: RateLimitingPlanId, insertEntries: Seq[RateLimitingPlanEntry]): SMono[RateLimitingPlanId] = - SFlux.fromIterable(insertEntries) - .flatMap(insertEntry => cassandraRateLimitPlanDAO.insert(insertEntry)) - .`then`() - .`then`(SMono.just(planId)) - - override def update(resetRequest: RateLimitingPlanResetRequest): Publisher[Unit] = - SMono.fromPublisher(cassandraRateLimitPlanDAO.planExists(resetRequest.id)) - .filter(exists => exists) - .switchIfEmpty(SMono.error(new RateLimitingPlanNotFoundException)) - .flatMap(_ => cassandraRateLimitPlanDAO.delete(resetRequest.id)) - .`then`(create(resetRequest.id, RateLimitingPlanEntry.from(resetRequest))) - .`then`() - - override def planExists(id: RateLimitingPlanId): Publisher[java.lang.Boolean] = cassandraRateLimitPlanDAO.planExists(id).map(boolean2Boolean) - - override def get(id: RateLimitingPlanId): Publisher[RateLimitingPlan] = - cassandraRateLimitPlanDAO.list(id) - .collectSeq() - .filter(_.nonEmpty) - .switchIfEmpty(SMono.error(new RateLimitingPlanNotFoundException)) - .map(convertEntriesToRateLimitingPlan) - - override def list(): Publisher[RateLimitingPlan] = - cassandraRateLimitPlanDAO.list() - .groupBy(planEntry => planEntry.planId) - .flatMap(planEntryGroupFlux => planEntryGroupFlux.collectSeq(), DEFAULT_CONCURRENCY) - .map(convertEntriesToRateLimitingPlan) - - private def convertEntriesToRateLimitingPlan(entries: Seq[RateLimitingPlanEntry]): RateLimitingPlan = { - Preconditions.checkArgument(entries.nonEmpty) - RateLimitingPlan(id = RateLimitingPlanId(entries.head.planId), - name = RateLimitingPlanName.liftOrThrow(entries.head.planName), - operationLimitations = entries.map(_.operationLimitations)) - } -} - diff --git a/tmail-backend/rate-limiter/rate-limiter-cassandra/src/main/scala/com/linagora/tmail/rate/limiter/api/cassandra/CassandraRateLimitingPlanUserRepository.scala b/tmail-backend/rate-limiter/rate-limiter-cassandra/src/main/scala/com/linagora/tmail/rate/limiter/api/cassandra/CassandraRateLimitingPlanUserRepository.scala deleted file mode 100644 index 3a006c6449..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-cassandra/src/main/scala/com/linagora/tmail/rate/limiter/api/cassandra/CassandraRateLimitingPlanUserRepository.scala +++ /dev/null @@ -1,52 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.cassandra - -import com.google.common.base.Preconditions -import com.linagora.tmail.rate.limiter.api.cassandra.dao.CassandraRateLimitPlanUserDAO -import com.linagora.tmail.rate.limiter.api.{RateLimitingPlanId, RateLimitingPlanNotFoundException, RateLimitingPlanUserRepository} -import jakarta.inject.Inject -import org.apache.james.core.Username -import reactor.core.scala.publisher.{SFlux, SMono} - -class CassandraRateLimitingPlanUserRepository @Inject()(dao: CassandraRateLimitPlanUserDAO) extends RateLimitingPlanUserRepository { - override def applyPlan(username: Username, planId: RateLimitingPlanId): SMono[Unit] = { - Preconditions.checkNotNull(username) - Preconditions.checkNotNull(planId) - dao.insertRecord(username, planId).`then`() - } - - override def revokePlan(username: Username): SMono[Unit] = { - Preconditions.checkNotNull(username) - dao.clearPlanId(username).`then`() - } - - override def listUsers(planId: RateLimitingPlanId): SFlux[Username] = { - Preconditions.checkNotNull(planId) - dao.getAllRecord - .filter(_.rateLimitingPlanId.equals(planId)) - .map(_.username) - } - - override def getPlanByUser(username: Username): SMono[RateLimitingPlanId] = { - Preconditions.checkNotNull(username) - dao.getPlanId(username) - .switchIfEmpty(SMono.error(RateLimitingPlanNotFoundException())) - } -} diff --git a/tmail-backend/rate-limiter/rate-limiter-cassandra/src/main/scala/com/linagora/tmail/rate/limiter/api/cassandra/dao/CassandraRateLimitPlanDAO.scala b/tmail-backend/rate-limiter/rate-limiter-cassandra/src/main/scala/com/linagora/tmail/rate/limiter/api/cassandra/dao/CassandraRateLimitPlanDAO.scala deleted file mode 100644 index 62a5adb3e0..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-cassandra/src/main/scala/com/linagora/tmail/rate/limiter/api/cassandra/dao/CassandraRateLimitPlanDAO.scala +++ /dev/null @@ -1,132 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.cassandra.dao - -import java.time.Duration -import java.util -import java.util.UUID - -import com.datastax.oss.driver.api.core.CqlSession -import com.datastax.oss.driver.api.core.`type`.{DataTypes, TupleType} -import com.datastax.oss.driver.api.core.cql.{PreparedStatement, Row} -import com.datastax.oss.driver.api.core.data.TupleValue -import com.datastax.oss.driver.api.querybuilder.QueryBuilder.{bindMarker, deleteFrom, insertInto, selectFrom} -import com.linagora.tmail.rate.limiter.api.cassandra.table.CassandraRateLimitPlanHeaderEntry.{RATE_LIMITATION_DURATION_INDEX, RATE_LIMITATION_NAME_INDEX, RATE_LIMITS_INDEX} -import com.linagora.tmail.rate.limiter.api.cassandra.table.CassandraRateLimitPlanTable.{OPERATION_LIMITATION_NAME, PLAN_ID, PLAN_NAME, RATE_LIMITATIONS, TABLE_NAME} -import com.linagora.tmail.rate.limiter.api.{LimitTypes, OperationLimitations, RateLimitation, RateLimitingPlanCreateRequest, RateLimitingPlanId, RateLimitingPlanResetRequest} -import jakarta.inject.Inject -import org.apache.james.backends.cassandra.utils.CassandraAsyncExecutor -import reactor.core.scala.publisher.{SFlux, SMono} - -import scala.jdk.CollectionConverters._ - -object RateLimitingPlanEntry { - def from(planId: RateLimitingPlanId, createRequest: RateLimitingPlanCreateRequest): Seq[RateLimitingPlanEntry] = - createRequest.operationLimitations.value - .map(operationLimitations => RateLimitingPlanEntry( - planId = planId.value, - planName = createRequest.name.value, - operationLimitations = operationLimitations)) - - def from(resetRequest: RateLimitingPlanResetRequest): Seq[RateLimitingPlanEntry] = - resetRequest.operationLimitations.value - .map(operationLimitations => RateLimitingPlanEntry( - planId = resetRequest.id.value, - planName = resetRequest.name.value, - operationLimitations = operationLimitations)) -} - -case class RateLimitingPlanEntry(planId: UUID, - planName: String, - operationLimitations: OperationLimitations) - -class CassandraRateLimitPlanDAO @Inject()(session: CqlSession) { - private val executor: CassandraAsyncExecutor = new CassandraAsyncExecutor(session) - private val rateLimitationsTuple: TupleType = DataTypes.tupleOf(DataTypes.TEXT, DataTypes.BIGINT, DataTypes.frozenMapOf(DataTypes.TEXT, DataTypes.BIGINT)) - - private val insertStatement: PreparedStatement = session.prepare(insertInto(TABLE_NAME) - .value(PLAN_ID, bindMarker(PLAN_ID)) - .value(PLAN_NAME, bindMarker(PLAN_NAME)) - .value(OPERATION_LIMITATION_NAME, bindMarker(OPERATION_LIMITATION_NAME)) - .value(RATE_LIMITATIONS, bindMarker(RATE_LIMITATIONS)) - .build()) - - private val deleteStatement: PreparedStatement = session.prepare(deleteFrom(TABLE_NAME) - .whereColumn(PLAN_ID).isEqualTo(bindMarker(PLAN_ID)) - .build()) - - private val selectStatement: PreparedStatement = session.prepare(selectFrom(TABLE_NAME) - .all() - .whereColumn(PLAN_ID).isEqualTo(bindMarker(PLAN_ID)) - .build()) - - private val selectAllStatement: PreparedStatement = session.prepare(selectFrom(TABLE_NAME).all().build()) - - def insert(insertEntry: RateLimitingPlanEntry): SMono[Void] = - SMono.fromPublisher(executor.executeVoid(insertStatement - .bind().setUuid(PLAN_ID, insertEntry.planId) - .setString(PLAN_NAME, insertEntry.planName) - .setString(OPERATION_LIMITATION_NAME, insertEntry.operationLimitations.asString()) - .setList(RATE_LIMITATIONS, toTupleList(insertEntry.operationLimitations.rateLimitations()), classOf[TupleValue]))) - - def delete(planId: RateLimitingPlanId): SMono[Void] = - SMono.fromPublisher(executor.executeVoid(deleteStatement.bind() - .setUuid(PLAN_ID, planId.value))) - - def list(planId: RateLimitingPlanId): SFlux[RateLimitingPlanEntry] = - SFlux.fromPublisher(executor.executeRows(selectStatement - .bind().setUuid(PLAN_ID, planId.value))) - .map(readRow) - - def planExists(planId: RateLimitingPlanId): SMono[Boolean] = - SMono.fromPublisher(executor.executeReturnExists(selectStatement - .bind().setUuid(PLAN_ID, planId.value))) - .map(_.booleanValue()) - - def list(): SFlux[RateLimitingPlanEntry] = - SFlux.fromPublisher(executor.executeRows(selectAllStatement.bind())) - .map(readRow) - - private def readRow(row: Row): RateLimitingPlanEntry = { - val rateLimitations: Seq[RateLimitation] = row.getList(RATE_LIMITATIONS, classOf[TupleValue]).asScala - .map(tupleData => { - val limits: Map[String, Long] = tupleData.getMap(RATE_LIMITS_INDEX, classOf[String], classOf[java.lang.Long]) - .asScala.map(pair => pair._1 -> pair._2.toLong) - .toMap - RateLimitation( - name = tupleData.getString(RATE_LIMITATION_NAME_INDEX), - period = Duration.ofMillis(tupleData.getLong(RATE_LIMITATION_DURATION_INDEX)), - limits = LimitTypes.from(limits)) - }).toSeq - - RateLimitingPlanEntry( - planId = row.getUuid(PLAN_ID), - planName = row.getString(PLAN_NAME), - operationLimitations = OperationLimitations.liftOrThrow(row.getString(OPERATION_LIMITATION_NAME), rateLimitations)) - } - - private def toTupleList(rateLimitations: Seq[RateLimitation]): java.util.List[TupleValue] = - rateLimitations.map(rateLimitation => { - val limits: util.Map[String, Long] = rateLimitation.limits.value - .map(limitType => limitType.asString() -> limitType.allowedQuantity().value) - .toMap.asJava - rateLimitationsTuple.newValue(rateLimitation.name, rateLimitation.period.toMillis, limits) - }).asJava - -} diff --git a/tmail-backend/rate-limiter/rate-limiter-cassandra/src/main/scala/com/linagora/tmail/rate/limiter/api/cassandra/dao/CassandraRateLimitPlanUserDAO.scala b/tmail-backend/rate-limiter/rate-limiter-cassandra/src/main/scala/com/linagora/tmail/rate/limiter/api/cassandra/dao/CassandraRateLimitPlanUserDAO.scala deleted file mode 100644 index 213a71de4c..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-cassandra/src/main/scala/com/linagora/tmail/rate/limiter/api/cassandra/dao/CassandraRateLimitPlanUserDAO.scala +++ /dev/null @@ -1,81 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.cassandra.dao - -import com.datastax.oss.driver.api.core.CqlSession -import com.datastax.oss.driver.api.core.`type`.codec.TypeCodecs -import com.datastax.oss.driver.api.core.cql.Row -import com.datastax.oss.driver.api.querybuilder.QueryBuilder.{bindMarker, insertInto, selectFrom, update} -import com.linagora.tmail.rate.limiter.api.{RateLimitingPlanId, UsernameToRateLimitingPlanId} -import com.linagora.tmail.user.cassandra.TMailCassandraUsersRepositoryDataDefinition.{RATE_LIMITING_PLAN_ID, TABLE_NAME, USER} -import jakarta.inject.Inject -import org.apache.james.backends.cassandra.utils.CassandraAsyncExecutor -import org.apache.james.core.Username -import reactor.core.scala.publisher.{SFlux, SMono} - -class CassandraRateLimitPlanUserDAO @Inject()(session: CqlSession) { - private val executor = new CassandraAsyncExecutor(session) - - private val insertStatement = session.prepare(insertInto(TABLE_NAME) - .value(USER, bindMarker(USER)) - .value(RATE_LIMITING_PLAN_ID, bindMarker(RATE_LIMITING_PLAN_ID)) - .build()) - - private val selectOnePlanIdStatement = session.prepare(selectFrom(TABLE_NAME).column(RATE_LIMITING_PLAN_ID) - .whereColumn(USER).isEqualTo(bindMarker(USER)) - .build()) - - private val selectAllStatement = session.prepare(selectFrom(TABLE_NAME) - .columns(USER, RATE_LIMITING_PLAN_ID) - .build()) - - private val clearPlanIdOfUser = session.prepare(update(TABLE_NAME) - .setColumn(RATE_LIMITING_PLAN_ID, bindMarker(RATE_LIMITING_PLAN_ID)) - .whereColumn(USER).isEqualTo(bindMarker(USER)) - .build()) - - def insertRecord(username: Username, rateLimitingPlanId: RateLimitingPlanId): SMono[Void] = - SMono.fromPublisher(executor.executeVoid(insertStatement.bind() - .setString(USER, username.asString) - .setUuid(RATE_LIMITING_PLAN_ID, rateLimitingPlanId.value))) - - def getPlanId(username: Username): SMono[RateLimitingPlanId] = - SMono.fromPublisher(executor.executeSingleRow(selectOnePlanIdStatement.bind() - .setString(USER, username.asString)) - .filter(associatedPlanIdExists) - .map(this.readPlanId)) - - def getAllRecord: SFlux[UsernameToRateLimitingPlanId] = - SFlux.fromPublisher(executor.executeRows(selectAllStatement.bind()) - .filter(associatedPlanIdExists) - .map(this.readRow)) - - def clearPlanId(username: Username): SMono[Void] = - SMono.fromPublisher(executor.executeVoid(clearPlanIdOfUser.bind() - .set(USER, username.asString, TypeCodecs.TEXT) - .setToNull(RATE_LIMITING_PLAN_ID))) - - private def readPlanId(row: Row): RateLimitingPlanId = RateLimitingPlanId(row.getUuid(RATE_LIMITING_PLAN_ID)) - - private def readRow(row: Row): UsernameToRateLimitingPlanId = UsernameToRateLimitingPlanId(Username.of(row.getString(USER)), - RateLimitingPlanId(row.getUuid(RATE_LIMITING_PLAN_ID))) - - private def associatedPlanIdExists(row: Row): Boolean = - row.getUuid(RATE_LIMITING_PLAN_ID) != null -} diff --git a/tmail-backend/rate-limiter/rate-limiter-cassandra/src/main/scala/com/linagora/tmail/rate/limiter/api/cassandra/module/CassandraRateLimitingModule.scala b/tmail-backend/rate-limiter/rate-limiter-cassandra/src/main/scala/com/linagora/tmail/rate/limiter/api/cassandra/module/CassandraRateLimitingModule.scala deleted file mode 100644 index 418870b1fe..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-cassandra/src/main/scala/com/linagora/tmail/rate/limiter/api/cassandra/module/CassandraRateLimitingModule.scala +++ /dev/null @@ -1,52 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.cassandra.module - -import com.google.inject.multibindings.Multibinder -import com.google.inject.{AbstractModule, Scopes} -import com.linagora.tmail.rate.limiter.api.cassandra.dao.{CassandraRateLimitPlanDAO, CassandraRateLimitPlanUserDAO} -import com.linagora.tmail.rate.limiter.api.cassandra.table.CassandraRateLimitPlanTable -import com.linagora.tmail.rate.limiter.api.cassandra.{CassandraRateLimitingPlanRepository, CassandraRateLimitingPlanUserRepository, CassandraRateLimitingRepository} -import com.linagora.tmail.rate.limiter.api.{RateLimitingPlanRepository, RateLimitingPlanUserRepository, RateLimitingPlanUsernameChangeTaskStep, RateLimitingRepository, RateLimitingUsernameChangeTaskStep} -import org.apache.james.backends.cassandra.components.CassandraDataDefinition -import org.apache.james.user.api.UsernameChangeTaskStep - -class CassandraRateLimitingModule() extends AbstractModule { - override def configure(): Unit = { - bind(classOf[CassandraRateLimitPlanDAO]).in(Scopes.SINGLETON) - bind(classOf[CassandraRateLimitPlanUserDAO]).in(Scopes.SINGLETON) - - bind(classOf[RateLimitingPlanUserRepository]).to(classOf[CassandraRateLimitingPlanUserRepository]) - bind(classOf[RateLimitingPlanRepository]).to(classOf[CassandraRateLimitingPlanRepository]) - - val multibinder = Multibinder.newSetBinder(binder, classOf[CassandraDataDefinition]) - multibinder.addBinding().toInstance(CassandraRateLimitPlanTable.MODULE) - - Multibinder.newSetBinder(binder(), classOf[UsernameChangeTaskStep]) - .addBinding() - .to(classOf[RateLimitingPlanUsernameChangeTaskStep]) - - bind(classOf[CassandraRateLimitingRepository]).in(Scopes.SINGLETON) - bind(classOf[RateLimitingRepository]).to(classOf[CassandraRateLimitingRepository]) - - Multibinder.newSetBinder(binder(), classOf[UsernameChangeTaskStep]) - .addBinding() - .to(classOf[RateLimitingUsernameChangeTaskStep]) - } -} diff --git a/tmail-backend/rate-limiter/rate-limiter-cassandra/src/main/scala/com/linagora/tmail/rate/limiter/api/cassandra/table/CassandraRateLimitPlanTable.scala b/tmail-backend/rate-limiter/rate-limiter-cassandra/src/main/scala/com/linagora/tmail/rate/limiter/api/cassandra/table/CassandraRateLimitPlanTable.scala deleted file mode 100644 index 01b0c21e64..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-cassandra/src/main/scala/com/linagora/tmail/rate/limiter/api/cassandra/table/CassandraRateLimitPlanTable.scala +++ /dev/null @@ -1,49 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.cassandra.table - -import com.datastax.oss.driver.api.core.`type`.DataTypes -import org.apache.james.backends.cassandra.components.CassandraDataDefinition - -object CassandraRateLimitPlanTable { - val TABLE_NAME: String = "rate_limit_plan" - val PLAN_ID: String = "plan_id" - val PLAN_NAME: String = "plan_name" - val OPERATION_LIMITATION_NAME: String = "operation_limitation_name" - val RATE_LIMITATIONS: String = "rate_limitations" - - val MODULE: CassandraDataDefinition = CassandraDataDefinition.table(TABLE_NAME) - .comment("Hold Rate Limiting Plan - Use to management.") - .statement(statement => types => statement - .withPartitionKey(PLAN_ID, DataTypes.UUID) - .withStaticColumn(PLAN_NAME, DataTypes.TEXT) - .withClusteringColumn(OPERATION_LIMITATION_NAME, DataTypes.TEXT) - .withColumn(RATE_LIMITATIONS, - DataTypes.frozenListOf(DataTypes.tupleOf( - DataTypes.TEXT, - DataTypes.BIGINT, - DataTypes.frozenMapOf(DataTypes.TEXT, DataTypes.BIGINT))))) - .build -} - -object CassandraRateLimitPlanHeaderEntry { - val RATE_LIMITATION_NAME_INDEX: Int = 0 - val RATE_LIMITATION_DURATION_INDEX: Int = 1 - val RATE_LIMITS_INDEX: Int = 2 -} diff --git a/tmail-backend/rate-limiter/rate-limiter-cassandra/src/test/java/com/linagora/tmail/rate/limiter/api/cassandra/CacheCassandraRateLimitingPlanRepositoryTest.java b/tmail-backend/rate-limiter/rate-limiter-cassandra/src/test/java/com/linagora/tmail/rate/limiter/api/cassandra/CacheCassandraRateLimitingPlanRepositoryTest.java deleted file mode 100644 index 4cfb60e48f..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-cassandra/src/test/java/com/linagora/tmail/rate/limiter/api/cassandra/CacheCassandraRateLimitingPlanRepositoryTest.java +++ /dev/null @@ -1,59 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.cassandra; - -import java.time.Duration; -import java.util.Optional; - -import org.apache.james.backends.cassandra.CassandraCluster; -import org.apache.james.backends.cassandra.CassandraClusterExtension; -import org.apache.james.backends.cassandra.components.CassandraDataDefinition; -import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionDataDefinition; -import org.apache.james.metrics.api.NoopGaugeRegistry; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.extension.RegisterExtension; - -import com.linagora.tmail.rate.limiter.api.CacheRateLimitingPlan; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanRepository; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanRepositoryContract; -import com.linagora.tmail.rate.limiter.api.cassandra.dao.CassandraRateLimitPlanDAO; -import com.linagora.tmail.rate.limiter.api.cassandra.table.CassandraRateLimitPlanTable; - -import scala.jdk.javaapi.OptionConverters; - -public class CacheCassandraRateLimitingPlanRepositoryTest implements RateLimitingPlanRepositoryContract { - - @RegisterExtension - static CassandraClusterExtension cassandraCluster = new CassandraClusterExtension( - CassandraDataDefinition.aggregateModules(CassandraRateLimitPlanTable.MODULE(), CassandraSchemaVersionDataDefinition.MODULE)); - - private CacheRateLimitingPlan repository; - - @BeforeEach - void setUp(CassandraCluster cassandra) { - CassandraRateLimitPlanDAO dao = new CassandraRateLimitPlanDAO(cassandra.getConf()); - RateLimitingPlanRepository rateLimitingPlanRepository = new CassandraRateLimitingPlanRepository(dao); - repository = new CacheRateLimitingPlan(rateLimitingPlanRepository, Duration.ofMinutes(2), new NoopGaugeRegistry(), OptionConverters.toScala(Optional.empty())); - } - - @Override - public RateLimitingPlanRepository testee() { - return repository; - } -} diff --git a/tmail-backend/rate-limiter/rate-limiter-cassandra/src/test/java/com/linagora/tmail/rate/limiter/api/cassandra/CassandraRateLimitingPlanRepositoryTest.java b/tmail-backend/rate-limiter/rate-limiter-cassandra/src/test/java/com/linagora/tmail/rate/limiter/api/cassandra/CassandraRateLimitingPlanRepositoryTest.java deleted file mode 100644 index 4b3ed3cd80..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-cassandra/src/test/java/com/linagora/tmail/rate/limiter/api/cassandra/CassandraRateLimitingPlanRepositoryTest.java +++ /dev/null @@ -1,51 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.cassandra; - -import org.apache.james.backends.cassandra.CassandraCluster; -import org.apache.james.backends.cassandra.CassandraClusterExtension; -import org.apache.james.backends.cassandra.components.CassandraDataDefinition; -import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionDataDefinition; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.extension.RegisterExtension; - -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanRepository; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanRepositoryContract; -import com.linagora.tmail.rate.limiter.api.cassandra.dao.CassandraRateLimitPlanDAO; -import com.linagora.tmail.rate.limiter.api.cassandra.table.CassandraRateLimitPlanTable; - -public class CassandraRateLimitingPlanRepositoryTest implements RateLimitingPlanRepositoryContract { - - @RegisterExtension - static CassandraClusterExtension cassandraCluster = new CassandraClusterExtension( - CassandraDataDefinition.aggregateModules(CassandraRateLimitPlanTable.MODULE(), CassandraSchemaVersionDataDefinition.MODULE)); - - private CassandraRateLimitingPlanRepository repository; - - @BeforeEach - void setUp(CassandraCluster cassandra) { - CassandraRateLimitPlanDAO dao = new CassandraRateLimitPlanDAO(cassandra.getConf()); - repository = new CassandraRateLimitingPlanRepository(dao); - } - - @Override - public RateLimitingPlanRepository testee() { - return repository; - } -} diff --git a/tmail-backend/rate-limiter/rate-limiter-cassandra/src/test/java/com/linagora/tmail/rate/limiter/api/cassandra/CassandraRateLimitingPlanUserRepositoryTest.java b/tmail-backend/rate-limiter/rate-limiter-cassandra/src/test/java/com/linagora/tmail/rate/limiter/api/cassandra/CassandraRateLimitingPlanUserRepositoryTest.java deleted file mode 100644 index d7fbe9c26e..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-cassandra/src/test/java/com/linagora/tmail/rate/limiter/api/cassandra/CassandraRateLimitingPlanUserRepositoryTest.java +++ /dev/null @@ -1,77 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.cassandra; - -import static org.apache.james.jmap.JMAPTestingConstants.BOB; -import static org.assertj.core.api.Assertions.assertThat; - -import org.apache.james.backends.cassandra.CassandraCluster; -import org.apache.james.backends.cassandra.CassandraClusterExtension; -import org.apache.james.backends.cassandra.components.CassandraDataDefinition; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import com.datastax.oss.driver.api.core.CqlSession; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanUserRepository; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanUserRepositoryContract; -import com.linagora.tmail.rate.limiter.api.cassandra.dao.CassandraRateLimitPlanUserDAO; -import com.linagora.tmail.user.cassandra.TMailCassandraUsersRepositoryDataDefinition; - -import reactor.core.publisher.Mono; - -public class CassandraRateLimitingPlanUserRepositoryTest implements RateLimitingPlanUserRepositoryContract { - @RegisterExtension - static CassandraClusterExtension cassandraCluster = new CassandraClusterExtension( - CassandraDataDefinition.aggregateModules(TMailCassandraUsersRepositoryDataDefinition.MODULE)); - - private CassandraRateLimitingPlanUserRepository repository; - - @BeforeEach - void setUp(CassandraCluster cassandra) { - CassandraRateLimitPlanUserDAO dao = new CassandraRateLimitPlanUserDAO(cassandra.getConf()); - repository = new CassandraRateLimitingPlanUserRepository(dao); - } - - @Override - public RateLimitingPlanUserRepository testee() { - return repository; - } - - @Test - void shouldNotDeleteUserRecordWhenRevokePlanId(CassandraCluster cassandraCluster) { - CqlSession testingSession = cassandraCluster.getConf(); - - // Given the Bob record in the user table - testingSession.execute(String.format("INSERT INTO user (name, realname) VALUES ('%s', '%s')", BOB.asString(), BOB.getLocalPart())); - - // Attach rate limiting plan for Bob - Mono.from(repository.applyPlan(BOB, RateLimitingPlanUserRepositoryContract.PLAN_ID_1())).block(); - - // Revoke Bob plan (e.g. as part of username change process) - Mono.from(repository.revokePlan(BOB)).block(); - - // Assert that the user record still exists after revoking plan, so other user associated data is not lost - assertThat(testingSession.execute(String.format("SELECT * FROM user WHERE name = '%s'", BOB.asString())) - .iterator() - .next() - .get("realname", String.class)) - .isEqualTo(BOB.getLocalPart()); - } -} diff --git a/tmail-backend/rate-limiter/rate-limiter-postgres/pom.xml b/tmail-backend/rate-limiter/rate-limiter-postgres/pom.xml index 081bbcf27a..7a84d5ff8f 100644 --- a/tmail-backend/rate-limiter/rate-limiter-postgres/pom.xml +++ b/tmail-backend/rate-limiter/rate-limiter-postgres/pom.xml @@ -60,20 +60,4 @@ test - - - - - net.alchim31.maven - scala-maven-plugin - - - io.github.evis - scalafix-maven-plugin_2.13 - - ${project.parent.parent.basedir}/.scalafix.conf - - - - \ No newline at end of file diff --git a/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/java/com/linagora/tmail/rate/limiter/api/postgres/PostgresRateLimitingPlanRepository.java b/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/java/com/linagora/tmail/rate/limiter/api/postgres/PostgresRateLimitingPlanRepository.java deleted file mode 100644 index 490086b4af..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/java/com/linagora/tmail/rate/limiter/api/postgres/PostgresRateLimitingPlanRepository.java +++ /dev/null @@ -1,94 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.postgres; - -import java.util.List; - -import jakarta.inject.Inject; - -import org.reactivestreams.Publisher; - -import com.google.common.base.Preconditions; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlan; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanCreateRequest; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanId; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanName; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanNotFoundException; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanRepository; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanResetRequest; -import com.linagora.tmail.rate.limiter.api.postgres.dao.PostgresRateLimitingPlanDAO; - -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import scala.jdk.javaapi.CollectionConverters; -import scala.runtime.BoxedUnit; - -public class PostgresRateLimitingPlanRepository implements RateLimitingPlanRepository { - private final PostgresRateLimitingPlanDAO rateLimitingPlanRepositoryDAO; - - @Inject - public PostgresRateLimitingPlanRepository(PostgresRateLimitingPlanDAO rateLimitingPlanRepositoryDAO) { - this.rateLimitingPlanRepositoryDAO = rateLimitingPlanRepositoryDAO; - } - - @Override - public Publisher create(RateLimitingPlanCreateRequest creationRequest) { - RateLimitingPlanId planId = RateLimitingPlanId.generate(); - return rateLimitingPlanRepositoryDAO.savePlans(RateLimitingPlanEntry.from(planId, creationRequest)) - .then(Mono.from(get(planId))); - } - - @Override - public Publisher update(RateLimitingPlanResetRequest resetRequest) { - return rateLimitingPlanRepositoryDAO.planExists(resetRequest.id()) - .filter(exists -> exists) - .flatMap(exists -> rateLimitingPlanRepositoryDAO.updatePlans(RateLimitingPlanEntry.from(resetRequest)) - .then(Mono.just(BoxedUnit.UNIT))) - .switchIfEmpty(Mono.error(new RateLimitingPlanNotFoundException())); - } - - @Override - public Publisher get(RateLimitingPlanId id) { - return rateLimitingPlanRepositoryDAO.getPlans(id) - .collectList() - .filter(rateLimitingPlanEntries -> !rateLimitingPlanEntries.isEmpty()) - .map(this::convertEntriesToRateLimitingPlan) - .switchIfEmpty(Mono.error(new RateLimitingPlanNotFoundException())); - } - - @Override - public Publisher planExists(RateLimitingPlanId id) { - return rateLimitingPlanRepositoryDAO.planExists(id); - } - - @Override - public Publisher list() { - return rateLimitingPlanRepositoryDAO.getPlans() - .groupBy(RateLimitingPlanEntry::planId) - .flatMap(Flux::collectList) - .map(this::convertEntriesToRateLimitingPlan); - } - - private RateLimitingPlan convertEntriesToRateLimitingPlan(List rateLimitingPlanEntries) { - Preconditions.checkArgument(!rateLimitingPlanEntries.isEmpty()); - return new RateLimitingPlan(new RateLimitingPlanId(rateLimitingPlanEntries.get(0).planId()), - RateLimitingPlanName.liftOrThrow(rateLimitingPlanEntries.get(0).planName()), - CollectionConverters.asScala(rateLimitingPlanEntries.stream().map(RateLimitingPlanEntry::operationLimitations).toList()).toSeq()); - } -} diff --git a/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/java/com/linagora/tmail/rate/limiter/api/postgres/RateLimitingPlanEntry.java b/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/java/com/linagora/tmail/rate/limiter/api/postgres/RateLimitingPlanEntry.java deleted file mode 100644 index 1af7c07d80..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/java/com/linagora/tmail/rate/limiter/api/postgres/RateLimitingPlanEntry.java +++ /dev/null @@ -1,93 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.postgres; - -import java.time.Duration; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.google.common.collect.ImmutableMap; -import com.linagora.tmail.rate.limiter.api.LimitType; -import com.linagora.tmail.rate.limiter.api.OperationLimitations; -import com.linagora.tmail.rate.limiter.api.RateLimitation; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanCreateRequest; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanId; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanResetRequest; -import com.linagora.tmail.rate.limiter.api.postgres.dao.PostgresRateLimitingDAOUtils; - -import scala.jdk.javaapi.CollectionConverters; - -public record RateLimitingPlanEntry(UUID planId, - String planName, - String operationLimitationName, - List rateLimitationsDTOS) { - - static List from(RateLimitingPlanId rateLimitingPlanId, RateLimitingPlanCreateRequest createRequest) { - return CollectionConverters.asJava(createRequest.operationLimitationsValue()).stream() - .map(limitations -> new RateLimitingPlanEntry(rateLimitingPlanId.value(), - createRequest.nameValue(), - limitations.asString(), - RateLimitationsDTO.listFrom(CollectionConverters.asJava(limitations.rateLimitations())))) - .toList(); - } - - static List from(RateLimitingPlanResetRequest resetRequest) { - return CollectionConverters.asJava(resetRequest.operationLimitationsValue()).stream() - .map(limitations -> new RateLimitingPlanEntry(resetRequest.id().value(), - resetRequest.nameValue(), - limitations.asString(), - RateLimitationsDTO.listFrom(CollectionConverters.asJava(limitations.rateLimitations())))) - .toList(); - } - - public record RateLimitationsDTO(@JsonProperty("rateLimitationName") String rateLimitationName, - @JsonProperty("rateLimitationPeriod") Long rateLimitationPeriod, - @JsonProperty("limitMap") Map limitMap) { - public static RateLimitationsDTO from(RateLimitation rateLimitation) { - return new RateLimitationsDTO(rateLimitation.name(), - rateLimitation.period().toMillis(), - CollectionConverters.asJava(rateLimitation.limitsValue()).stream() - .collect(ImmutableMap.toImmutableMap(LimitType::asString, - PostgresRateLimitingDAOUtils::getQuantity))); - } - - public static List listFrom(List rateLimitations) { - return rateLimitations.stream() - .map(RateLimitationsDTO::from) - .toList(); - } - - public RateLimitation asRateLimitation() { - return new RateLimitation(rateLimitationName(), - Duration.ofMillis(rateLimitationPeriod()), - PostgresRateLimitingDAOUtils.getLimitType(limitMap())); - } - } - - public OperationLimitations operationLimitations() { - return OperationLimitations.liftOrThrow( - operationLimitationName(), - CollectionConverters.asScala(rateLimitationsDTOS().stream() - .map(RateLimitationsDTO::asRateLimitation) - .toList()).toSeq()); - } - -} diff --git a/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/java/com/linagora/tmail/rate/limiter/api/postgres/dao/PostgresRateLimitingPlanDAO.java b/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/java/com/linagora/tmail/rate/limiter/api/postgres/dao/PostgresRateLimitingPlanDAO.java deleted file mode 100644 index a497a29a01..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/java/com/linagora/tmail/rate/limiter/api/postgres/dao/PostgresRateLimitingPlanDAO.java +++ /dev/null @@ -1,117 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.postgres.dao; - -import static com.linagora.tmail.rate.limiter.api.postgres.table.PostgresRateLimitPlanModule.PostgresRateLimitPlanTable.OPERATION_LIMITATION_NAME; -import static com.linagora.tmail.rate.limiter.api.postgres.table.PostgresRateLimitPlanModule.PostgresRateLimitPlanTable.PLAN_ID; -import static com.linagora.tmail.rate.limiter.api.postgres.table.PostgresRateLimitPlanModule.PostgresRateLimitPlanTable.PLAN_NAME; -import static com.linagora.tmail.rate.limiter.api.postgres.table.PostgresRateLimitPlanModule.PostgresRateLimitPlanTable.RATE_LIMITATIONS; -import static com.linagora.tmail.rate.limiter.api.postgres.table.PostgresRateLimitPlanModule.PostgresRateLimitPlanTable.TABLE_NAME; - -import java.util.List; -import java.util.function.Function; - -import jakarta.inject.Inject; - -import org.apache.james.backends.postgres.utils.PostgresExecutor; -import org.jooq.JSON; -import org.jooq.Record; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanId; -import com.linagora.tmail.rate.limiter.api.postgres.RateLimitingPlanEntry; -import com.linagora.tmail.rate.limiter.api.postgres.RateLimitingPlanEntry.RateLimitationsDTO; - -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -public class PostgresRateLimitingPlanDAO { - private final PostgresExecutor postgresExecutor; - private final ObjectMapper objectMapper; - - @Inject - public PostgresRateLimitingPlanDAO(PostgresExecutor postgresExecutor) { - this.postgresExecutor = postgresExecutor; - objectMapper = new ObjectMapper(); - } - - public Mono savePlans(List planEntryList) { - return postgresExecutor.executeVoid(dslContext -> Mono.from(dslContext.batch(planEntryList - .stream() - .map(planEntry -> dslContext.insertInto(TABLE_NAME) - .set(PLAN_ID, planEntry.planId()) - .set(PLAN_NAME, planEntry.planName()) - .set(OPERATION_LIMITATION_NAME, planEntry.operationLimitationName()) - .set(RATE_LIMITATIONS, toJSON(planEntry.rateLimitationsDTOS()))) - .toList()))); - } - - public Mono updatePlans(List planEntryList) { - return postgresExecutor.executeVoid(dslContext -> Mono.from(dslContext.batch(planEntryList - .stream() - .map(planEntry -> dslContext.update(TABLE_NAME) - .set(PLAN_NAME, planEntry.planName()) - .set(OPERATION_LIMITATION_NAME, planEntry.operationLimitationName()) - .set(RATE_LIMITATIONS, toJSON(planEntry.rateLimitationsDTOS())) - .where(PLAN_ID.eq(planEntry.planId()))) - .toList()))); - } - - public Flux getPlans(RateLimitingPlanId planId) { - return postgresExecutor.executeRows(dslContext -> Flux.from(dslContext.selectFrom(TABLE_NAME) - .where(PLAN_ID.eq(planId.value())))) - .map(recordRateLimitingPlanEntryFunction()); - } - - public Flux getPlans() { - return postgresExecutor.executeRows(dslContext -> Flux.from(dslContext.selectFrom(TABLE_NAME))) - .map(recordRateLimitingPlanEntryFunction()); - } - - private Function recordRateLimitingPlanEntryFunction() { - return record -> new RateLimitingPlanEntry(record.get(PLAN_ID), - record.get(PLAN_NAME), - record.get(OPERATION_LIMITATION_NAME), - toRateLimitationsDTOList(record.get(RATE_LIMITATIONS))); - } - - public Mono planExists(RateLimitingPlanId id) { - return postgresExecutor.executeExists(dslContext -> dslContext.select(PLAN_ID) - .from(TABLE_NAME) - .where(PLAN_ID.eq(id.value()))); - } - - private JSON toJSON(List rateLimitations) { - try { - return JSON.json(objectMapper.writeValueAsString(rateLimitations)); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - private List toRateLimitationsDTOList(JSON json) { - try { - return objectMapper.readValue(json.data(), new TypeReference>() {}); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } -} diff --git a/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/model/OperationLimitationsDTO.java b/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/java/com/linagora/tmail/rate/limiter/api/postgres/module/PostgresRateLimitingModule.java similarity index 57% rename from tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/model/OperationLimitationsDTO.java rename to tmail-backend/rate-limiter/rate-limiter-postgres/src/main/java/com/linagora/tmail/rate/limiter/api/postgres/module/PostgresRateLimitingModule.java index 124d886bc4..82d26c8c69 100644 --- a/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/model/OperationLimitationsDTO.java +++ b/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/java/com/linagora/tmail/rate/limiter/api/postgres/module/PostgresRateLimitingModule.java @@ -16,25 +16,25 @@ * more details. * ********************************************************************/ -package com.linagora.tmail.webadmin.model; +package com.linagora.tmail.rate.limiter.api.postgres.module; -import java.util.List; +import org.apache.james.user.api.UsernameChangeTaskStep; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; -import com.google.common.base.Preconditions; +import com.google.inject.AbstractModule; +import com.google.inject.Scopes; +import com.google.inject.multibindings.Multibinder; +import com.linagora.tmail.rate.limiter.api.RateLimitingRepository; +import com.linagora.tmail.rate.limiter.api.RateLimitingUsernameChangeTaskStep; +import com.linagora.tmail.rate.limiter.api.postgres.PostgresRateLimitingRepository; -public class OperationLimitationsDTO { - private final List rateLimitationDTOList; +public class PostgresRateLimitingModule extends AbstractModule { + @Override + protected void configure() { + bind(PostgresRateLimitingRepository.class).in(Scopes.SINGLETON); + bind(RateLimitingRepository.class).to(PostgresRateLimitingRepository.class); - @JsonCreator - public OperationLimitationsDTO(List rateLimitationDTOList) { - Preconditions.checkArgument(!rateLimitationDTOList.isEmpty(), "Operation limitation arrays must have at least one entry."); - this.rateLimitationDTOList = rateLimitationDTOList; - } - - @JsonValue - public List getRateLimitationDTOList() { - return rateLimitationDTOList; + Multibinder.newSetBinder(binder(), UsernameChangeTaskStep.class) + .addBinding() + .to(RateLimitingUsernameChangeTaskStep.class); } } diff --git a/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/java/com/linagora/tmail/rate/limiter/api/postgres/table/PostgresRateLimitPlanModule.java b/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/java/com/linagora/tmail/rate/limiter/api/postgres/table/PostgresRateLimitPlanModule.java deleted file mode 100644 index f14a129ee6..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/java/com/linagora/tmail/rate/limiter/api/postgres/table/PostgresRateLimitPlanModule.java +++ /dev/null @@ -1,64 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.postgres.table; - -import static com.linagora.tmail.rate.limiter.api.postgres.table.PostgresRateLimitPlanModule.PostgresRateLimitPlanTable.PLAN_ID_INDEX; -import static com.linagora.tmail.rate.limiter.api.postgres.table.PostgresRateLimitPlanModule.PostgresRateLimitPlanTable.TABLE; - -import java.util.UUID; - -import org.apache.james.backends.postgres.PostgresDataDefinition; -import org.apache.james.backends.postgres.PostgresIndex; -import org.apache.james.backends.postgres.PostgresTable; -import org.jooq.Field; -import org.jooq.JSON; -import org.jooq.Record; -import org.jooq.Table; -import org.jooq.impl.DSL; -import org.jooq.impl.SQLDataType; - -public interface PostgresRateLimitPlanModule { - interface PostgresRateLimitPlanTable { - Table TABLE_NAME = DSL.table("rate_limit_plan"); - - Field PLAN_ID = DSL.field("plan_id", SQLDataType.UUID.notNull()); - Field PLAN_NAME = DSL.field("plan_name", SQLDataType.VARCHAR.notNull()); - Field OPERATION_LIMITATION_NAME = DSL.field("operation_limitation_name", SQLDataType.VARCHAR.notNull()); - Field RATE_LIMITATIONS = DSL.field("rate_limitations", SQLDataType.JSON.notNull()); - - PostgresTable TABLE = PostgresTable.name(TABLE_NAME.getName()) - .createTableStep(((dsl, tableName) -> dsl.createTableIfNotExists(tableName) - .column(PLAN_ID) - .column(PLAN_NAME) - .column(OPERATION_LIMITATION_NAME) - .column(RATE_LIMITATIONS) - .primaryKey(PLAN_ID, OPERATION_LIMITATION_NAME))) - .disableRowLevelSecurity() - .build(); - - PostgresIndex PLAN_ID_INDEX = PostgresIndex.name("rate_limit_plan_plan_id_index") - .createIndexStep((dsl, indexName) -> dsl.createIndexIfNotExists(indexName) - .on(TABLE_NAME, PLAN_ID)); - } - - PostgresDataDefinition MODULE = PostgresDataDefinition.builder() - .addTable(TABLE) - .addIndex(PLAN_ID_INDEX) - .build(); -} diff --git a/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/scala/com/linagora/tmail/rate/limiter/api/postgres/dao/PostgresRateLimitPlanUserDAO.scala b/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/scala/com/linagora/tmail/rate/limiter/api/postgres/dao/PostgresRateLimitPlanUserDAO.scala deleted file mode 100644 index a7ff6ac59f..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/scala/com/linagora/tmail/rate/limiter/api/postgres/dao/PostgresRateLimitPlanUserDAO.scala +++ /dev/null @@ -1,57 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.postgres.dao - -import java.util.UUID - -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanId -import com.linagora.tmail.user.postgres.TMailPostgresUserDataDefinition.PostgresUserTable.{RATE_LIMITING_PLAN_ID, TABLE_NAME, USERNAME} -import jakarta.inject.Inject -import org.apache.james.backends.postgres.utils.PostgresExecutor -import org.apache.james.core.Username -import reactor.core.publisher.{Flux, Mono} - -class PostgresRateLimitPlanUserDAO @Inject()(postgresExecutor: PostgresExecutor) { - def insert(username: Username, rateLimitingPlanId: RateLimitingPlanId): Mono[Void] = - postgresExecutor.executeVoid(dsl => Mono.from(dsl.insertInto(TABLE_NAME) - .set(USERNAME, username.asString()) - .set(RATE_LIMITING_PLAN_ID, rateLimitingPlanId.value) - .onConflict(USERNAME) - .doUpdate() - .set(RATE_LIMITING_PLAN_ID, rateLimitingPlanId.value))) - - def getPlanIdByUser(username: Username): Mono[RateLimitingPlanId] = - postgresExecutor.executeRow(dsl => Mono.from(dsl.select(RATE_LIMITING_PLAN_ID) - .from(TABLE_NAME) - .where(USERNAME.eq(username.asString())))) - .filter(_.get(RATE_LIMITING_PLAN_ID) != null) - .map(record => RateLimitingPlanId(record.get(RATE_LIMITING_PLAN_ID))) - - def getUsersByPlanId(planId: RateLimitingPlanId): Flux[Username] = - postgresExecutor.executeRows(dsl => Flux.from(dsl.select(USERNAME) - .from(TABLE_NAME) - .where(RATE_LIMITING_PLAN_ID.eq(planId.value)))) - .filter(_.get(USERNAME) != null) - .map(record => Username.of(record.get(USERNAME))) - - def clearPlan(username: Username): Mono[Void] = - postgresExecutor.executeVoid(dsl => Mono.from(dsl.update(TABLE_NAME) - .set(RATE_LIMITING_PLAN_ID, null.asInstanceOf[UUID]) - .where(USERNAME.eq(username.asString())))) -} diff --git a/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/scala/com/linagora/tmail/rate/limiter/api/postgres/dao/PostgresRateLimitingDAOUtils.scala b/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/scala/com/linagora/tmail/rate/limiter/api/postgres/dao/PostgresRateLimitingDAOUtils.scala deleted file mode 100644 index 3c9eb78f42..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/scala/com/linagora/tmail/rate/limiter/api/postgres/dao/PostgresRateLimitingDAOUtils.scala +++ /dev/null @@ -1,37 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.postgres.dao - -import com.linagora.tmail.rate.limiter.api.LimitTypes.LimitTypes -import com.linagora.tmail.rate.limiter.api.{LimitType, LimitTypes} - -import scala.jdk.CollectionConverters._ - -object PostgresRateLimitingDAOUtils { - def getQuantity(limitType: LimitType): Long = { - limitType.allowedQuantity().value.longValue - } - - def getLimitType(limitTypeAndAllowedQuantity: java.util.Map[String, java.lang.Long]): LimitTypes = { - val limitTypes = limitTypeAndAllowedQuantity.asScala - .map(map => LimitType.liftOrThrow(map._1, map._2.toLong)) - .toSet - LimitTypes.liftOrThrow(limitTypes) - } -} diff --git a/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/scala/com/linagora/tmail/rate/limiter/api/postgres/module/PostgresRateLimitingModule.scala b/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/scala/com/linagora/tmail/rate/limiter/api/postgres/module/PostgresRateLimitingModule.scala deleted file mode 100644 index b4f71abbeb..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/scala/com/linagora/tmail/rate/limiter/api/postgres/module/PostgresRateLimitingModule.scala +++ /dev/null @@ -1,52 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.postgres.module - -import com.google.inject.multibindings.Multibinder -import com.google.inject.{AbstractModule, Scopes} -import com.linagora.tmail.rate.limiter.api.postgres.repository.PostgresRateLimitingPlanUserRepository -import com.linagora.tmail.rate.limiter.api.postgres.table.PostgresRateLimitPlanModule -import com.linagora.tmail.rate.limiter.api.postgres.{PostgresRateLimitingPlanRepository, PostgresRateLimitingRepository} -import com.linagora.tmail.rate.limiter.api.{RateLimitingPlanRepository, RateLimitingPlanUserRepository, RateLimitingPlanUsernameChangeTaskStep, RateLimitingRepository, RateLimitingUsernameChangeTaskStep} -import org.apache.james.backends.postgres.PostgresDataDefinition -import org.apache.james.user.api.UsernameChangeTaskStep - -class PostgresRateLimitingModule() extends AbstractModule { - override def configure(): Unit = { - bind(classOf[PostgresRateLimitingPlanUserRepository]).in(Scopes.SINGLETON) - bind(classOf[PostgresRateLimitingPlanRepository]).in(Scopes.SINGLETON) - - bind(classOf[RateLimitingPlanUserRepository]).to(classOf[PostgresRateLimitingPlanUserRepository]) - bind(classOf[RateLimitingPlanRepository]).to(classOf[PostgresRateLimitingPlanRepository]) - - val postgresRateLimitingDataDefinitions = Multibinder.newSetBinder(binder, classOf[PostgresDataDefinition]) - postgresRateLimitingDataDefinitions.addBinding().toInstance(PostgresRateLimitPlanModule.MODULE) - - Multibinder.newSetBinder(binder(), classOf[UsernameChangeTaskStep]) - .addBinding() - .to(classOf[RateLimitingPlanUsernameChangeTaskStep]) - - bind(classOf[PostgresRateLimitingRepository]).in(Scopes.SINGLETON) - bind(classOf[RateLimitingRepository]).to(classOf[PostgresRateLimitingRepository]) - - Multibinder.newSetBinder(binder(), classOf[UsernameChangeTaskStep]) - .addBinding() - .to(classOf[RateLimitingUsernameChangeTaskStep]) - } -} diff --git a/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/scala/com/linagora/tmail/rate/limiter/api/postgres/repository/PostgresRateLimitingPlanUserRepository.scala b/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/scala/com/linagora/tmail/rate/limiter/api/postgres/repository/PostgresRateLimitingPlanUserRepository.scala deleted file mode 100644 index 9d79ac7d59..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-postgres/src/main/scala/com/linagora/tmail/rate/limiter/api/postgres/repository/PostgresRateLimitingPlanUserRepository.scala +++ /dev/null @@ -1,59 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.postgres.repository - -import com.google.common.base.Preconditions -import com.linagora.tmail.rate.limiter.api.postgres.dao.PostgresRateLimitPlanUserDAO -import com.linagora.tmail.rate.limiter.api.{RateLimitingPlanId, RateLimitingPlanNotFoundException, RateLimitingPlanUserRepository} -import jakarta.inject.Inject -import org.apache.james.core.Username -import org.reactivestreams.Publisher -import reactor.core.publisher.Mono -import reactor.core.scala.publisher.SMono - -class PostgresRateLimitingPlanUserRepository @Inject()(dao: PostgresRateLimitPlanUserDAO) extends RateLimitingPlanUserRepository { - - override def applyPlan(username: Username, planId: RateLimitingPlanId): Publisher[Unit] = { - Preconditions.checkNotNull(username) - Preconditions.checkNotNull(planId) - - SMono(dao.insert(username, planId)) - .`then`() - } - - override def revokePlan(username: Username): Publisher[Unit] = { - Preconditions.checkNotNull(username) - - SMono(dao.clearPlan(username)) - .`then`() - } - - override def listUsers(planId: RateLimitingPlanId): Publisher[Username] = { - Preconditions.checkNotNull(planId) - - dao.getUsersByPlanId(planId) - } - - override def getPlanByUser(username: Username): Publisher[RateLimitingPlanId] = { - Preconditions.checkNotNull(username) - - dao.getPlanIdByUser(username) - .switchIfEmpty(Mono.error(() => RateLimitingPlanNotFoundException())) - } -} diff --git a/tmail-backend/rate-limiter/rate-limiter-postgres/src/test/java/com/linagora/tmail/rate/limiter/api/postgres/PostgresRateLimitingPlanRepositoryTest.java b/tmail-backend/rate-limiter/rate-limiter-postgres/src/test/java/com/linagora/tmail/rate/limiter/api/postgres/PostgresRateLimitingPlanRepositoryTest.java deleted file mode 100644 index 074748a6e3..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-postgres/src/test/java/com/linagora/tmail/rate/limiter/api/postgres/PostgresRateLimitingPlanRepositoryTest.java +++ /dev/null @@ -1,37 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.postgres; - -import org.apache.james.backends.postgres.PostgresExtension; -import org.junit.jupiter.api.extension.RegisterExtension; - -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanRepository; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanRepositoryContract; -import com.linagora.tmail.rate.limiter.api.postgres.dao.PostgresRateLimitingPlanDAO; -import com.linagora.tmail.rate.limiter.api.postgres.table.PostgresRateLimitPlanModule; - -public class PostgresRateLimitingPlanRepositoryTest implements RateLimitingPlanRepositoryContract { - @RegisterExtension - static PostgresExtension postgresExtension = PostgresExtension.withoutRowLevelSecurity(PostgresRateLimitPlanModule.MODULE); - - @Override - public RateLimitingPlanRepository testee() { - return new PostgresRateLimitingPlanRepository(new PostgresRateLimitingPlanDAO(postgresExtension.getDefaultPostgresExecutor())); - } -} diff --git a/tmail-backend/rate-limiter/rate-limiter-postgres/src/test/java/com/linagora/tmail/rate/limiter/api/postgres/PostgresRateLimitingPlanUserRepositoryTest.java b/tmail-backend/rate-limiter/rate-limiter-postgres/src/test/java/com/linagora/tmail/rate/limiter/api/postgres/PostgresRateLimitingPlanUserRepositoryTest.java deleted file mode 100644 index 434130b6fd..0000000000 --- a/tmail-backend/rate-limiter/rate-limiter-postgres/src/test/java/com/linagora/tmail/rate/limiter/api/postgres/PostgresRateLimitingPlanUserRepositoryTest.java +++ /dev/null @@ -1,79 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.rate.limiter.api.postgres; - -import static org.apache.james.jmap.JMAPTestingConstants.BOB; -import static org.assertj.core.api.Assertions.assertThat; - -import org.apache.james.backends.postgres.PostgresDataDefinition; -import org.apache.james.backends.postgres.PostgresExtension; -import org.jooq.impl.DSL; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanUserRepository; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanUserRepositoryContract; -import com.linagora.tmail.rate.limiter.api.postgres.dao.PostgresRateLimitPlanUserDAO; -import com.linagora.tmail.rate.limiter.api.postgres.repository.PostgresRateLimitingPlanUserRepository; -import com.linagora.tmail.user.postgres.TMailPostgresUserDataDefinition; - -import reactor.core.publisher.Mono; - -class PostgresRateLimitingPlanUserRepositoryTest implements RateLimitingPlanUserRepositoryContract { - @RegisterExtension - static PostgresExtension postgresExtension = PostgresExtension.withoutRowLevelSecurity( - PostgresDataDefinition.aggregateModules(TMailPostgresUserDataDefinition.MODULE)); - - private PostgresRateLimitingPlanUserRepository repository; - - @BeforeEach - void setUp() { - repository = new PostgresRateLimitingPlanUserRepository(new PostgresRateLimitPlanUserDAO(postgresExtension.getDefaultPostgresExecutor())); - } - - @Override - public RateLimitingPlanUserRepository testee() { - return repository; - } - - @Test - void shouldNotDeleteUserRecordWhenDeleteSettings() { - // Given the Bob record in the user table - postgresExtension.getDefaultPostgresExecutor() - .executeVoid(dslContext -> Mono.from(dslContext.insertInto(DSL.table("users"), DSL.field("username"), DSL.field("hashed_password")) - .values(BOB.asString(), "hashedPassword"))) - .block(); - - // Set Bob rate limiting plan - Mono.from(repository.applyPlan(BOB, RateLimitingPlanUserRepositoryContract.PLAN_ID_1())).block(); - - // Revoke Bob rate limiting plan (e.g. as part of username change process) - Mono.from(repository.revokePlan(BOB)).block(); - - // Assert that the user record still exists after revoke associated plan, so other user associated data is not lost - String storedHashPassword = postgresExtension.getDefaultPostgresExecutor() - .executeRow(dslContext -> Mono.from(dslContext.select(DSL.field("hashed_password")) - .from(DSL.table("users")) - .where(DSL.field("username").eq(BOB.asString())))) - .block() - .get("hashed_password", String.class); - assertThat(storedHashPassword).isEqualTo("hashedPassword"); - } -} diff --git a/tmail-backend/webadmin/webadmin-rate-limit/pom.xml b/tmail-backend/webadmin/webadmin-rate-limit/pom.xml index aefd6d6963..c3f831ce18 100644 --- a/tmail-backend/webadmin/webadmin-rate-limit/pom.xml +++ b/tmail-backend/webadmin/webadmin-rate-limit/pom.xml @@ -96,20 +96,4 @@ test - - - - - net.alchim31.maven - scala-maven-plugin - - - io.github.evis - scalafix-maven-plugin_2.13 - - ${project.parent.parent.basedir}/.scalafix.conf - - - - \ No newline at end of file diff --git a/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/RateLimitPlanUserRoutes.java b/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/RateLimitPlanUserRoutes.java deleted file mode 100644 index f742304102..0000000000 --- a/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/RateLimitPlanUserRoutes.java +++ /dev/null @@ -1,161 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.webadmin; - -import static org.apache.james.webadmin.Constants.SEPARATOR; -import static org.eclipse.jetty.http.HttpStatus.NOT_FOUND_404; - -import jakarta.inject.Inject; - -import org.apache.james.core.Username; -import org.apache.james.user.api.UsersRepository; -import org.apache.james.user.api.UsersRepositoryException; -import org.apache.james.webadmin.Routes; -import org.apache.james.webadmin.utils.ErrorResponder; -import org.apache.james.webadmin.utils.JsonTransformer; -import org.apache.james.webadmin.utils.Responses; - -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanId; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanNotFoundException; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanRepository; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanUserRepository; -import com.linagora.tmail.webadmin.model.RateLimitingPlanIdResponse; - -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import spark.Request; -import spark.Route; -import spark.Service; - -public class RateLimitPlanUserRoutes implements Routes { - private static final String USERNAME_PARAM = ":username"; - private static final String PLAN_ID_PARAM = ":planId"; - private static final String RATE_LIMIT_BASE_PATH = SEPARATOR + "rate-limit-plans"; - private static final String USERS_PATH = SEPARATOR + "users"; - private static final String ATTACH_PLAN_TO_USER_PATH = USERS_PATH + SEPARATOR + USERNAME_PARAM + RATE_LIMIT_BASE_PATH - + SEPARATOR + PLAN_ID_PARAM; - private static final String GET_USERS_OF_PLAN_PATH = RATE_LIMIT_BASE_PATH + SEPARATOR + PLAN_ID_PARAM + USERS_PATH; - private static final String REVOKE_PLAN_OF_USER_PATH = USERS_PATH + SEPARATOR + USERNAME_PARAM + RATE_LIMIT_BASE_PATH; - private static final String GET_PLAN_OF_USER_PATH = USERS_PATH + SEPARATOR + USERNAME_PARAM + RATE_LIMIT_BASE_PATH; - - private final RateLimitingPlanUserRepository planUserRepository; - private final RateLimitingPlanRepository planRepository; - private final UsersRepository usersRepository; - private final JsonTransformer jsonTransformer; - - @Inject - public RateLimitPlanUserRoutes(RateLimitingPlanUserRepository planUserRepository, RateLimitingPlanRepository planRepository, - UsersRepository usersRepository, JsonTransformer jsonTransformer) { - this.planUserRepository = planUserRepository; - this.planRepository = planRepository; - this.usersRepository = usersRepository; - this.jsonTransformer = jsonTransformer; - } - - @Override - public String getBasePath() { - return RATE_LIMIT_BASE_PATH; - } - - @Override - public void define(Service service) { - service.put(ATTACH_PLAN_TO_USER_PATH, attachPlanToUser(), jsonTransformer); - service.get(GET_USERS_OF_PLAN_PATH, getUsersOfPlan(), jsonTransformer); - service.get(GET_PLAN_OF_USER_PATH, getPlanOfUser(), jsonTransformer); - service.delete(REVOKE_PLAN_OF_USER_PATH, revokePlanOfUser(), jsonTransformer); - } - - private Route attachPlanToUser() { - return (request, response) -> { - Username username = extractUsername(request); - RateLimitingPlanId planId = extractPlanId(request); - userPreconditions(username); - planIdPreconditions(planId); - return Mono.from(planUserRepository.applyPlan(username, planId)) - .then(Mono.just(Responses.returnNoContent(response))) - .block(); - }; - } - - private Route getUsersOfPlan() { - return (request, response) -> { - RateLimitingPlanId planId = extractPlanId(request); - planIdPreconditions(planId); - return Flux.from(planUserRepository.listUsers(planId)) - .map(Username::asString) - .collectList() - .block(); - }; - } - - private Route getPlanOfUser() { - return (request, response) -> { - Username username = extractUsername(request); - userPreconditions(username); - return Mono.from(planUserRepository.getPlanByUser(username)) - .map(planId -> new RateLimitingPlanIdResponse(planId.value().toString())) - .onErrorResume(RateLimitingPlanNotFoundException.class, e -> { - throw ErrorResponder.builder() - .statusCode(NOT_FOUND_404) - .type(ErrorResponder.ErrorType.NOT_FOUND) - .message(String.format("User %s does not have a plan", username.asString())) - .haltError(); - }) - .block(); - }; - } - - private Route revokePlanOfUser() { - return (request, response) -> { - Username username = extractUsername(request); - userPreconditions(username); - return Mono.from(planUserRepository.revokePlan(username)) - .then(Mono.just(Responses.returnNoContent(response))) - .block(); - }; - } - - private Username extractUsername(Request request) { - return Username.of(request.params(USERNAME_PARAM)); - } - - private RateLimitingPlanId extractPlanId(Request request) { - return RateLimitingPlanId.parse(request.params(PLAN_ID_PARAM)); - } - - public void userPreconditions(Username username) throws UsersRepositoryException { - if (!usersRepository.contains(username)) { - throw ErrorResponder.builder() - .statusCode(NOT_FOUND_404) - .type(ErrorResponder.ErrorType.NOT_FOUND) - .message(String.format("User %s does not exist", username.asString())) - .haltError(); - } - } - - public void planIdPreconditions(RateLimitingPlanId planId) { - if (!Mono.from(planRepository.planExists(planId)).block()) { - throw ErrorResponder.builder() - .statusCode(NOT_FOUND_404) - .type(ErrorResponder.ErrorType.NOT_FOUND) - .message(String.format("Plan id %s does not exist", planId.value().toString())) - .haltError(); - } - } -} diff --git a/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/RateLimitPlanRoutesModule.java b/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/RateLimitsRoutesModule.java similarity index 87% rename from tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/RateLimitPlanRoutesModule.java rename to tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/RateLimitsRoutesModule.java index aeb6a2c113..c1ca008bc9 100644 --- a/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/RateLimitPlanRoutesModule.java +++ b/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/RateLimitsRoutesModule.java @@ -23,12 +23,10 @@ import com.google.inject.AbstractModule; import com.google.inject.multibindings.Multibinder; -public class RateLimitPlanRoutesModule extends AbstractModule { +public class RateLimitsRoutesModule extends AbstractModule { @Override protected void configure() { Multibinder routesMultibinder = Multibinder.newSetBinder(binder(), Routes.class); routesMultibinder.addBinding().to(RateLimitsUserRoutes.class); - routesMultibinder.addBinding().to(RateLimitPlanUserRoutes.class); - routesMultibinder.addBinding().to(RateLimitPlanManagementRoutes.class); } } diff --git a/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/model/RateLimitingPlanCreateRequestDTO.java b/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/model/RateLimitingPlanCreateRequestDTO.java deleted file mode 100644 index a1b0ad7972..0000000000 --- a/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/model/RateLimitingPlanCreateRequestDTO.java +++ /dev/null @@ -1,28 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.webadmin.model; - -import java.util.HashMap; -import java.util.Optional; - -public class RateLimitingPlanCreateRequestDTO extends HashMap> { - public static final String TRANSIT_LIMIT_KEY = "transitLimits"; - public static final String DELIVERY_LIMIT_KEY = "deliveryLimits"; - public static final String RELAY_LIMIT_KEY = "relayLimits"; -} diff --git a/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/model/RateLimitingPlanDTO.java b/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/model/RateLimitingPlanDTO.java deleted file mode 100644 index 456e8f86a9..0000000000 --- a/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/model/RateLimitingPlanDTO.java +++ /dev/null @@ -1,30 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.webadmin.model; - -import java.util.Optional; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public record RateLimitingPlanDTO(@JsonProperty("planId") String planId, - @JsonProperty("planName") String planName, - @JsonProperty("transitLimits") Optional transitLimits, - @JsonProperty("relayLimits") Optional relayLimits, - @JsonProperty("deliveryLimits") Optional deliveryLimits) { -} diff --git a/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/model/RateLimitingPlanIdResponse.java b/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/model/RateLimitingPlanIdResponse.java deleted file mode 100644 index a51964257e..0000000000 --- a/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/model/RateLimitingPlanIdResponse.java +++ /dev/null @@ -1,22 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.webadmin.model; - -public record RateLimitingPlanIdResponse(String planId) { -} diff --git a/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/model/RateLimitingPlanResetRequestDTO.java b/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/model/RateLimitingPlanResetRequestDTO.java deleted file mode 100644 index d475b525bc..0000000000 --- a/tmail-backend/webadmin/webadmin-rate-limit/src/main/java/com/linagora/tmail/webadmin/model/RateLimitingPlanResetRequestDTO.java +++ /dev/null @@ -1,44 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.webadmin.model; - -import static com.linagora.tmail.webadmin.model.RateLimitingPlanCreateRequestDTO.DELIVERY_LIMIT_KEY; -import static com.linagora.tmail.webadmin.model.RateLimitingPlanCreateRequestDTO.RELAY_LIMIT_KEY; -import static com.linagora.tmail.webadmin.model.RateLimitingPlanCreateRequestDTO.TRANSIT_LIMIT_KEY; - -import java.util.Optional; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -public record RateLimitingPlanResetRequestDTO(String planName, - Optional transitLimits, - Optional relayLimits, - Optional deliveryLimits) { - @JsonCreator - public RateLimitingPlanResetRequestDTO(@JsonProperty(value = "planName", required = true) String planName, - @JsonProperty(TRANSIT_LIMIT_KEY) Optional transitLimits, - @JsonProperty(RELAY_LIMIT_KEY) Optional relayLimits, - @JsonProperty(DELIVERY_LIMIT_KEY) Optional deliveryLimits) { - this.planName = planName; - this.transitLimits = transitLimits; - this.relayLimits = relayLimits; - this.deliveryLimits = deliveryLimits; - } -} diff --git a/tmail-backend/webadmin/webadmin-rate-limit/src/main/scala/com/linagora/tmail/webadmin/RateLimitPlanManagementRoutes.scala b/tmail-backend/webadmin/webadmin-rate-limit/src/main/scala/com/linagora/tmail/webadmin/RateLimitPlanManagementRoutes.scala deleted file mode 100644 index e786e575bf..0000000000 --- a/tmail-backend/webadmin/webadmin-rate-limit/src/main/scala/com/linagora/tmail/webadmin/RateLimitPlanManagementRoutes.scala +++ /dev/null @@ -1,168 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.webadmin - -import java.time.Duration -import java.util.Optional - -import com.linagora.tmail.rate.limiter.api.LimitTypes.LimitTypes -import com.linagora.tmail.rate.limiter.api.OperationLimitations.{DELIVERY_LIMITATIONS_NAME, RELAY_LIMITATIONS_NAME, TRANSIT_LIMITATIONS_NAME} -import com.linagora.tmail.rate.limiter.api.{LimitTypes, OperationLimitations, OperationLimitationsType, RateLimitation, RateLimitingPlan, RateLimitingPlanCreateRequest, RateLimitingPlanId, RateLimitingPlanName, RateLimitingPlanNotFoundException, RateLimitingPlanRepository, RateLimitingPlanResetRequest} -import com.linagora.tmail.webadmin.model.RateLimitingPlanCreateRequestDTO.{DELIVERY_LIMIT_KEY, RELAY_LIMIT_KEY, TRANSIT_LIMIT_KEY} -import com.linagora.tmail.webadmin.model.{GetAllRateLimitPlanResponseDTO, OperationLimitationsDTO, RateLimitationDTO, RateLimitingPlanCreateRequestDTO, RateLimitingPlanDTO, RateLimitingPlanIdResponse, RateLimitingPlanResetRequestDTO} -import jakarta.inject.Inject -import org.apache.james.webadmin.Constants.SEPARATOR -import org.apache.james.webadmin.Routes -import org.apache.james.webadmin.utils.{ErrorResponder, JsonExtractor, JsonTransformer, Responses} -import org.eclipse.jetty.http.HttpStatus.{CREATED_201, NOT_FOUND_404} -import reactor.core.scala.publisher.{SFlux, SMono} -import spark.{Request, Response, Route, Service} - -import scala.jdk.CollectionConverters._ -import scala.jdk.OptionConverters._ -import scala.jdk.StreamConverters._ - -class RateLimitPlanManagementRoutes @Inject()(planRepository: RateLimitingPlanRepository, - jsonTransformer: JsonTransformer) extends Routes { - private val PLAN_ID_PARAM = ":planId" - private val PLAN_NAME_PARAM = ":planName" - private val BASE_PATH = s"${SEPARATOR}rate-limit-plans" - private val CREATE_A_PLAN_PATH = s"$BASE_PATH$SEPARATOR$PLAN_NAME_PARAM" - private val UPDATE_A_PLAN_PATH = s"$BASE_PATH$SEPARATOR$PLAN_ID_PARAM" - private val GET_A_PLAN_PATH = s"$BASE_PATH$SEPARATOR$PLAN_ID_PARAM" - private val GET_ALL_PLAN_PATH = BASE_PATH - - val createRequestExtractor = new JsonExtractor[RateLimitingPlanCreateRequestDTO](classOf[RateLimitingPlanCreateRequestDTO]) - val resetRequestExtractor = new JsonExtractor[RateLimitingPlanResetRequestDTO](classOf[RateLimitingPlanResetRequestDTO]) - - override def getBasePath: String = BASE_PATH - - override def define(service: Service): Unit = { - service.post(CREATE_A_PLAN_PATH, createAPlan, jsonTransformer) - service.put(UPDATE_A_PLAN_PATH, updateAPlan, jsonTransformer) - service.get(GET_A_PLAN_PATH, getAPlan, jsonTransformer) - service.get(GET_ALL_PLAN_PATH, getAllPlan, jsonTransformer) - } - - private def createAPlan: Route = (request: Request, response: Response) => - SMono.fromPublisher(planRepository.create(toCreateRequest(request))) - .map(plan => { - response.status(CREATED_201) - new RateLimitingPlanIdResponse(plan.id.serialize()) - }) - .block() - - private def updateAPlan: Route = (request: Request, response: Response) => - SMono.fromPublisher(planRepository.update(toResetRequest(request))) - .onErrorResume { case e: RateLimitingPlanNotFoundException => SMono.error( - ErrorResponder.builder() - .statusCode(NOT_FOUND_404) - .`type`(ErrorResponder.ErrorType.NOT_FOUND) - .message("Plan does not exist") - .haltError()) - } - .`then`(SMono.just(Responses.returnNoContent(response))) - .block() - - private def getAPlan: Route = (request: Request, response: Response) => - SMono.fromPublisher(planRepository.get(extractRateLimitingPlanId(request))) - .map(toRateLimitingPlanDTO) - .onErrorResume { case e: RateLimitingPlanNotFoundException => SMono.error( - ErrorResponder.builder() - .statusCode(NOT_FOUND_404) - .`type`(ErrorResponder.ErrorType.NOT_FOUND) - .message("Plan does not exist") - .haltError()) - } - .block() - - private def getAllPlan: Route = (request: Request, response: Response) => - SFlux.fromPublisher(planRepository.list()) - .map(toRateLimitingPlanDTO) - .collectSeq() - .map(seq => new GetAllRateLimitPlanResponseDTO(seq.toList.asJava)) - .block() - - private def toRateLimitingPlanDTO(rateLimitingPlan: RateLimitingPlan): RateLimitingPlanDTO = - new RateLimitingPlanDTO(rateLimitingPlan.id.serialize(), - rateLimitingPlan.name.value, - toOperationLimitationsDTO(rateLimitingPlan.operationLimitations, OperationLimitations.TRANSIT_LIMITATIONS_NAME), - toOperationLimitationsDTO(rateLimitingPlan.operationLimitations, OperationLimitations.RELAY_LIMITATIONS_NAME), - toOperationLimitationsDTO(rateLimitingPlan.operationLimitations, OperationLimitations.DELIVERY_LIMITATIONS_NAME)) - - private def toOperationLimitationsDTO(operationLimitation: Seq[OperationLimitations], operationType: String): Optional[OperationLimitationsDTO] = - operationLimitation.find(_.asString().equals(operationType)) - .map(transitLimitation => new OperationLimitationsDTO(transitLimitation.rateLimitations().map(toRateLimitationDTO).toList.asJava)) - .toJava - - private def toRateLimitationDTO(rateLimitation: RateLimitation): RateLimitationDTO = - new RateLimitationDTO(rateLimitation.name, rateLimitation.period.getSeconds, - extractLimitType(rateLimitation, LimitTypes.COUNT), extractLimitType(rateLimitation, LimitTypes.SIZE)) - - private def extractLimitType(rateLimitation: RateLimitation, typeString: String): Long = - rateLimitation.limits.value - .filter(_.asString().equals(typeString)) - .head - .allowedQuantity() - .value - - private def toCreateRequest(request: Request): RateLimitingPlanCreateRequest = { - val createRequestDTO = createRequestExtractor.parse(request.body()) - val combinedLimitations = java.util.stream.Stream.of( - createRequestDTO.getOrDefault(TRANSIT_LIMIT_KEY, Optional.empty()).map(toOperationLimitation(TRANSIT_LIMITATIONS_NAME, _)), - createRequestDTO.getOrDefault(DELIVERY_LIMIT_KEY, Optional.empty()).map(toOperationLimitation(DELIVERY_LIMITATIONS_NAME, _)), - createRequestDTO.getOrDefault(RELAY_LIMIT_KEY, Optional.empty()).map(toOperationLimitation(RELAY_LIMITATIONS_NAME, _))) - .filter(_.isPresent) - .map(_.get) - .toScala(Seq) - - RateLimitingPlanCreateRequest(extractRateLimitingPlanName(request), OperationLimitationsType.liftOrThrow(combinedLimitations)) - } - - private def toResetRequest(request: Request): RateLimitingPlanResetRequest = { - val resetRequestDTO = resetRequestExtractor.parse(request.body()) - val combinedLimitations = java.util.stream.Stream.of( - resetRequestDTO.transitLimits.map(toOperationLimitation(TRANSIT_LIMITATIONS_NAME, _)), - resetRequestDTO.deliveryLimits.map(toOperationLimitation(DELIVERY_LIMITATIONS_NAME, _)), - resetRequestDTO.relayLimits.map(toOperationLimitation(RELAY_LIMITATIONS_NAME, _))) - .filter(_.isPresent) - .map(_.get) - .toScala(Seq) - - RateLimitingPlanResetRequest(extractRateLimitingPlanId(request), RateLimitingPlanName.liftOrThrow(resetRequestDTO.planName), - OperationLimitationsType.liftOrThrow(combinedLimitations)) - } - - private def toOperationLimitation(operationName: String, dto: OperationLimitationsDTO): OperationLimitations = - OperationLimitations.liftOrThrow(operationName, dto.getRateLimitationDTOList - .stream() - .map(rateLimitationDTO => toRateLimitation(rateLimitationDTO)) - .toScala(Seq)) - - private def toRateLimitation(dto: RateLimitationDTO): RateLimitation = { - require(dto.periodInSeconds.>=(0), "Rate limitation period must not be negative") - RateLimitation(name = dto.name, period = Duration.ofSeconds(dto.periodInSeconds), limits = toLimitTypes(dto.count, dto.size)) - } - - private def toLimitTypes(count: Long, size: Long): LimitTypes = LimitTypes.from(Map((LimitTypes.COUNT, count), (LimitTypes.SIZE, size))) - - private def extractRateLimitingPlanId(request: Request) = RateLimitingPlanId.parse(request.params(PLAN_ID_PARAM)) - - private def extractRateLimitingPlanName(request: Request) = RateLimitingPlanName.liftOrThrow(request.params(PLAN_NAME_PARAM)) -} diff --git a/tmail-backend/webadmin/webadmin-rate-limit/src/test/java/com/linagora/tmail/webadmin/RateLimitPlanManagementRoutesTest.java b/tmail-backend/webadmin/webadmin-rate-limit/src/test/java/com/linagora/tmail/webadmin/RateLimitPlanManagementRoutesTest.java deleted file mode 100644 index 9b5c046d3a..0000000000 --- a/tmail-backend/webadmin/webadmin-rate-limit/src/test/java/com/linagora/tmail/webadmin/RateLimitPlanManagementRoutesTest.java +++ /dev/null @@ -1,912 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.webadmin; - -import static io.restassured.RestAssured.given; -import static io.restassured.http.ContentType.JSON; -import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; -import static org.assertj.core.api.Assertions.assertThat; -import static org.eclipse.jetty.http.HttpStatus.BAD_REQUEST_400; -import static org.eclipse.jetty.http.HttpStatus.CREATED_201; -import static org.eclipse.jetty.http.HttpStatus.NOT_FOUND_404; -import static org.eclipse.jetty.http.HttpStatus.NO_CONTENT_204; -import static org.eclipse.jetty.http.HttpStatus.OK_200; - -import java.util.Map; - -import org.apache.james.webadmin.WebAdminServer; -import org.apache.james.webadmin.WebAdminUtils; -import org.apache.james.webadmin.utils.JsonTransformer; -import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import com.linagora.tmail.rate.limiter.api.InMemoryRateLimitingPlanRepository; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanRepository; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanId; - -import io.restassured.RestAssured; -import reactor.core.publisher.Mono; - -public class RateLimitPlanManagementRoutesTest { - private static final String CREATE_A_PLAN_PATH = "/rate-limit-plans/%s"; - private static final String UPDATE_A_PLAN_PATH = "/rate-limit-plans/%s"; - private static final String GET_A_PLAN_PATH = "/rate-limit-plans/%s"; - private static final String GET_ALL_PLAN_PATH = "/rate-limit-plans"; - - private WebAdminServer webAdminServer; - private RateLimitingPlanRepository planRepository; - - @BeforeEach - void setUp() { - planRepository = new InMemoryRateLimitingPlanRepository(); - RateLimitPlanManagementRoutes routes = new RateLimitPlanManagementRoutes(planRepository, new JsonTransformer()); - webAdminServer = WebAdminUtils.createWebAdminServer(routes).start(); - - RestAssured.requestSpecification = WebAdminUtils.buildRequestSpecification(webAdminServer).build(); - } - - @AfterEach - void tearDown() { - webAdminServer.destroy(); - } - - @Nested - class CreateAPlanTest { - @Test - void shouldSucceed() { - String json = """ - { - "transitLimits": [{ - "name": "receivedMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }, - { - "name": "receivedMailsPerDay", - "periodInSeconds": 86400, - "count": 1000, - "size": 4096 - } - ], - "relayLimits": [{ - "name": "relayMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }], - "deliveryLimits": [{ - "name": "deliveryMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }] - }"""; - - Map response = given() - .body(json) - .post(String.format(CREATE_A_PLAN_PATH, "planName1")) - .then() - .statusCode(CREATED_201) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(response).containsOnlyKeys("planId"); - } - - @Test - void shouldSucceedWhenOnlyTransitLimits() { - String json = """ - { - "transitLimits": [{ - "name": "receivedMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }, - { - "name": "receivedMailsPerDay", - "periodInSeconds": 86400, - "count": 1000, - "size": 4096 - } - ] - }"""; - - Map response = given() - .body(json) - .post(String.format(CREATE_A_PLAN_PATH, "planName1")) - .then() - .statusCode(CREATED_201) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(response).containsOnlyKeys("planId"); - } - - @Test - void shouldReturnBadRequestWhenEmptyPayLoad() { - String json = "{}"; - - Map errors = given() - .body(json) - .post(String.format(CREATE_A_PLAN_PATH, "planName1")) - .then() - .statusCode(BAD_REQUEST_400) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", BAD_REQUEST_400) - .containsEntry("type", "InvalidArgument") - .containsEntry("message", "Invalid arguments supplied in the user request") - .containsEntry("details", "value should not be empty"); - } - - @Test - void shouldReturnBadRequestWhenAllEntryAreNull() { - String json = """ - { - "transitLimits": null, - "relayLimits": null, - "deliveryLimits": null - }"""; - - Map errors = given() - .body(json) - .post(String.format(CREATE_A_PLAN_PATH, "planName1")) - .then() - .statusCode(BAD_REQUEST_400) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", BAD_REQUEST_400) - .containsEntry("type", "InvalidArgument") - .containsEntry("message", "Invalid arguments supplied in the user request") - .containsEntry("details", "value should not be empty"); - } - - @Test - void shouldReturnBadRequestWhenAEntryIsEmptyArray() { - String json = "{\"transitLimits\":[]}"; - - Map errors = given() - .body(json) - .post(String.format(CREATE_A_PLAN_PATH, "planName1")) - .then() - .statusCode(BAD_REQUEST_400) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", BAD_REQUEST_400) - .containsEntry("type", "InvalidArgument") - .containsEntry("message", "JSON payload of the request is not valid"); - assertThat(errors.get("details").toString()).contains("Operation limitation arrays must have at least one entry."); - } - - @Test - void shouldReturnBadRequestWhenMissingRateLimitationNameField() { - String json = """ - { - "transitLimits": [{ - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }] - }"""; - - Map errors = given() - .body(json) - .post(String.format(CREATE_A_PLAN_PATH, "planName1")) - .then() - .statusCode(BAD_REQUEST_400) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(errors) - .containsEntry("statusCode", BAD_REQUEST_400) - .containsEntry("type", "InvalidArgument") - .containsEntry("message", "JSON payload of the request is not valid"); - softly.assertThat(errors.get("details").toString()).contains("Missing required creator property 'name'"); - }); - } - - @Test - void shouldReturnBadRequestWhenMissingRateLimitationPeriodField() { - String json = """ - { - "transitLimits": [{ - "name": "receivedMailsPerHour", - "count": 100, - "size": 2048 - }] - }"""; - - Map errors = given() - .body(json) - .post(String.format(CREATE_A_PLAN_PATH, "planName1")) - .then() - .statusCode(BAD_REQUEST_400) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(errors) - .containsEntry("statusCode", BAD_REQUEST_400) - .containsEntry("type", "InvalidArgument") - .containsEntry("message", "JSON payload of the request is not valid"); - softly.assertThat(errors.get("details").toString()).contains("Missing required creator property 'periodInSeconds'"); - }); - } - - @Test - void shouldReturnBadRequestWhenMissingRateLimitationCountField() { - String json = """ - { - "transitLimits": [{ - "name": "receivedMailsPerHour", - "periodInSeconds": 3600, - "size": 2048 - }] - }"""; - - Map errors = given() - .body(json) - .post(String.format(CREATE_A_PLAN_PATH, "planName1")) - .then() - .statusCode(BAD_REQUEST_400) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(errors) - .containsEntry("statusCode", BAD_REQUEST_400) - .containsEntry("type", "InvalidArgument") - .containsEntry("message", "JSON payload of the request is not valid"); - softly.assertThat(errors.get("details").toString()).contains("Missing required creator property 'count'"); - }); - } - - @Test - void shouldReturnBadRequestWhenNegativeCount() { - String json = """ - { - "transitLimits": [{ - "name": "receivedMailsPerHour", - "periodInSeconds": 3600, - "count": -100, - "size": 2048 - }] - }"""; - - Map errors = given() - .body(json) - .post(String.format(CREATE_A_PLAN_PATH, "planName1")) - .then() - .statusCode(BAD_REQUEST_400) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", BAD_REQUEST_400) - .containsEntry("type", "InvalidArgument") - .containsEntry("message", "Invalid arguments supplied in the user request") - .containsEntry("details", "Predicate failed: (-100 > 0)."); - } - - @Test - void shouldReturnBadRequestWhenMissingRateLimitationSizeField() { - String json = """ - { - "transitLimits": [{ - "name": "receivedMailsPerHour", - "periodInSeconds": 3600, - "count": 100 - }] - }"""; - - Map errors = given() - .body(json) - .post(String.format(CREATE_A_PLAN_PATH, "planName1")) - .then() - .statusCode(BAD_REQUEST_400) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(errors) - .containsEntry("statusCode", BAD_REQUEST_400) - .containsEntry("type", "InvalidArgument") - .containsEntry("message", "JSON payload of the request is not valid"); - softly.assertThat(errors.get("details").toString()).contains("Missing required creator property 'size'"); - }); - } - - @Test - void shouldReturnBadRequestWhenNegativeSize() { - String json = """ - { - "transitLimits": [{ - "name": "receivedMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": -2048 - }] - }"""; - - Map errors = given() - .body(json) - .post(String.format(CREATE_A_PLAN_PATH, "planName1")) - .then() - .statusCode(BAD_REQUEST_400) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", BAD_REQUEST_400) - .containsEntry("type", "InvalidArgument") - .containsEntry("message", "Invalid arguments supplied in the user request") - .containsEntry("details", "Predicate failed: (-2048 > 0)."); - } - - @Test - void shouldReturnBadRequestWhenNegativePeriod() { - String json = """ - { - "transitLimits": [{ - "name": "receivedMailsPerHour", - "periodInSeconds": -3600, - "count": 100, - "size": 2048 - }] - }"""; - - Map errors = given() - .body(json) - .post(String.format(CREATE_A_PLAN_PATH, "planName1")) - .then() - .statusCode(BAD_REQUEST_400) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", BAD_REQUEST_400) - .containsEntry("type", "InvalidArgument") - .containsEntry("message", "Invalid arguments supplied in the user request") - .containsEntry("details", "requirement failed: Rate limitation period must not be negative"); - } - - @Test - void shouldSucceedWhenValidPeriod() { - String json = """ - { - "transitLimits": [{ - "name": "receivedMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }] - }"""; - - given() - .body(json) - .post(String.format(CREATE_A_PLAN_PATH, "planName1")) - .then() - .statusCode(CREATED_201) - .contentType(JSON); - } - } - - @Nested - class UpdateAPlanTest { - @Test - void shouldSucceedWhenPlanExists() { - String createPlanJson = """ - { - "transitLimits": [{ - "name": "receivedMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }, - { - "name": "receivedMailsPerDay", - "periodInSeconds": 86400, - "count": 1000, - "size": 4096 - } - ], - "relayLimits": [{ - "name": "relayMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }], - "deliveryLimits": [{ - "name": "deliveryMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }] - }"""; - - String oldPlanId = given() - .body(createPlanJson) - .post(String.format(CREATE_A_PLAN_PATH, "oldPlanName")) - .then() - .statusCode(CREATED_201) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getString("planId"); - - String updatePlanJson = """ - { - "planName": "newPlanName", - "transitLimits": [{ - "name": "receivedMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }, - { - "name": "receivedMailsPerDay", - "periodInSeconds": 86400, - "count": 1000, - "size": 4096 - } - ], - "relayLimits": [{ - "name": "relayMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }], - "deliveryLimits": null - }"""; - - String response = given() - .body(updatePlanJson) - .put(String.format(UPDATE_A_PLAN_PATH, oldPlanId)) - .then() - .statusCode(NO_CONTENT_204) - .contentType(JSON) - .extract() - .body() - .asString(); - - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(response).isEmpty(); - softly.assertThat(Mono.from(planRepository.get(RateLimitingPlanId.parse(oldPlanId))).block().name()) - .isEqualTo("newPlanName"); - }); - } - - @Test - void shouldReturnNotFoundWhenPlanNotFound() { - String json = """ - { - "planName": "newPlanName", - "transitLimits": [{ - "name": "receivedMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }, - { - "name": "receivedMailsPerDay", - "periodInSeconds": 86400, - "count": 1000, - "size": 4096 - } - ], - "relayLimits": [{ - "name": "relayMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }], - "deliveryLimits": null - }"""; - - Map errors = given() - .body(json) - .put(String.format(UPDATE_A_PLAN_PATH, "fbeb01f9-2f88-4c0d-8542-0cf576d5081d")) - .then() - .statusCode(NOT_FOUND_404) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", NOT_FOUND_404) - .containsEntry("type", "notFound") - .containsEntry("message", "Plan does not exist"); - } - - @Test - void shouldReturnBadRequestWhenMissingPlanName() { - String json = """ - { - "transitLimits": null, - "relayLimits": [{ - "name": "relayMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }], - "deliveryLimits": null - }"""; - - Map errors = given() - .body(json) - .put(String.format(UPDATE_A_PLAN_PATH, "fbeb01f9-2f88-4c0d-8542-0cf576d5081d")) - .then() - .statusCode(BAD_REQUEST_400) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", BAD_REQUEST_400) - .containsEntry("type", "InvalidArgument") - .containsEntry("message", "JSON payload of the request is not valid"); - } - - @Test - void shouldReturnBadRequestWhenPlanNameIsEmpty() { - String json = """ - { - "planName": "", - "transitLimits": [{ - "name": "receivedMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }], - "relayLimits": null, - "deliveryLimits": null - }"""; - - Map errors = given() - .body(json) - .put(String.format(UPDATE_A_PLAN_PATH, "fbeb01f9-2f88-4c0d-8542-0cf576d5081d")) - .then() - .statusCode(BAD_REQUEST_400) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", BAD_REQUEST_400) - .containsEntry("type", "InvalidArgument") - .containsEntry("message", "Invalid arguments supplied in the user request") - .containsEntry("details", "Rate limiting plan name should not be empty"); - } - } - - @Nested - class GetAPlanTest { - @Test - void shouldSucceedWhenPlanExists() { - String createPlanJson = """ - { - "transitLimits": [{ - "name": "receivedMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }, - { - "name": "receivedMailsPerDay", - "periodInSeconds": 86400, - "count": 1000, - "size": 4096 - } - ], - "relayLimits": [{ - "name": "relayMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }], - "deliveryLimits": [{ - "name": "deliveryMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }] - }"""; - - String oldPlanId = given() - .body(createPlanJson) - .post(String.format(CREATE_A_PLAN_PATH, "oldPlanName")) - .then() - .statusCode(CREATED_201) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getString("planId"); - - String response = given() - .get(String.format(GET_A_PLAN_PATH, oldPlanId)) - .then() - .statusCode(OK_200) - .contentType(JSON) - .extract() - .body() - .asString(); - - assertThatJson(response) - .whenIgnoringPaths("planId") - .isEqualTo(""" - { - "planId": "18828c8d-35c3-4ac8-bfac-1d3c0588b40c", - "planName": "oldPlanName", - "transitLimits": [{ - "name": "receivedMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }, - { - "name": "receivedMailsPerDay", - "periodInSeconds": 86400, - "count": 1000, - "size": 4096 - } - ], - "relayLimits": [{ - "name": "relayMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }], - "deliveryLimits": [{ - "name": "deliveryMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }] - }"""); - } - - @Test - void shouldReturnNotFoundWhenPlanNotFound() { - Map errors = given() - .get(String.format(GET_A_PLAN_PATH, "fbeb01f9-2f88-4c0d-8542-0cf576d5081d")) - .then() - .statusCode(NOT_FOUND_404) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", NOT_FOUND_404) - .containsEntry("type", "notFound") - .containsEntry("message", "Plan does not exist"); - } - - @Test - void shouldReturnBadRequestWhenInvalidPlanId() { - Map errors = given() - .get(String.format(GET_A_PLAN_PATH, "invalid planId")) - .then() - .statusCode(BAD_REQUEST_400) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", BAD_REQUEST_400) - .containsEntry("type", "InvalidArgument") - .containsEntry("message", "Invalid arguments supplied in the user request") - .containsEntry("details", "Invalid UUID string: invalid planId"); - } - } - - @Nested - class GetAllPlanTest { - @Test - void shouldReturnPlansWhenPlansExist() { - String createPlanJson = """ - { - "transitLimits": [{ - "name": "receivedMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }, - { - "name": "receivedMailsPerDay", - "periodInSeconds": 86400, - "count": 1000, - "size": 4096 - } - ], - "relayLimits": [{ - "name": "relayMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }], - "deliveryLimits": [{ - "name": "deliveryMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }] - }"""; - - given() - .body(createPlanJson) - .post(String.format(CREATE_A_PLAN_PATH, "plan1")) - .then() - .statusCode(CREATED_201) - .contentType(JSON); - - given() - .body(createPlanJson) - .post(String.format(CREATE_A_PLAN_PATH, "plan2")) - .then() - .statusCode(CREATED_201) - .contentType(JSON); - - String response = given() - .get(GET_ALL_PLAN_PATH) - .then() - .statusCode(OK_200) - .contentType(JSON) - .extract() - .body() - .asString(); - - assertThatJson(response) - .whenIgnoringPaths("[0].planId", "[0].planName", "[1].planId", "[1].planName") - .isEqualTo(""" - [{ - "planId": "02d64e08-488c-4094-87c8-a511b242e802", - "planName": "plan2", - "transitLimits": [{ - "name": "receivedMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }, - { - "name": "receivedMailsPerDay", - "periodInSeconds": 86400, - "count": 1000, - "size": 4096 - } - ], - "relayLimits": [{ - "name": "relayMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }], - "deliveryLimits": [{ - "name": "deliveryMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }] - }, - { - "planId": "5bb06b65-1b54-4ff4-8bad-ec5f425d9db6", - "planName": "plan1", - "transitLimits": [{ - "name": "receivedMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }, - { - "name": "receivedMailsPerDay", - "periodInSeconds": 86400, - "count": 1000, - "size": 4096 - } - ], - "relayLimits": [{ - "name": "relayMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }], - "deliveryLimits": [{ - "name": "deliveryMailsPerHour", - "periodInSeconds": 3600, - "count": 100, - "size": 2048 - }] - } - ]"""); - } - - @Test - void shouldReturnEmptyByDefault() { - String response = given() - .get(GET_ALL_PLAN_PATH) - .then() - .statusCode(OK_200) - .contentType(JSON) - .extract() - .body() - .asString(); - - assertThatJson(response).isArray().isEmpty(); - } - } -} diff --git a/tmail-backend/webadmin/webadmin-rate-limit/src/test/java/com/linagora/tmail/webadmin/RateLimitPlanUsersRoutesTest.java b/tmail-backend/webadmin/webadmin-rate-limit/src/test/java/com/linagora/tmail/webadmin/RateLimitPlanUsersRoutesTest.java deleted file mode 100644 index a11763ab10..0000000000 --- a/tmail-backend/webadmin/webadmin-rate-limit/src/test/java/com/linagora/tmail/webadmin/RateLimitPlanUsersRoutesTest.java +++ /dev/null @@ -1,512 +0,0 @@ -/******************************************************************** - * As a subpart of Twake Mail, this file is edited by Linagora. * - * * - * https://twake-mail.com/ * - * https://linagora.com * - * * - * This file is subject to The Affero Gnu Public License * - * version 3. * - * * - * https://www.gnu.org/licenses/agpl-3.0.en.html * - * * - * This program is distributed in the hope that it will be * - * useful, but WITHOUT ANY WARRANTY; without even the implied * - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * - * PURPOSE. See the GNU Affero General Public License for * - * more details. * - ********************************************************************/ - -package com.linagora.tmail.webadmin; - -import static io.restassured.RestAssured.given; -import static io.restassured.http.ContentType.JSON; -import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.eclipse.jetty.http.HttpStatus.BAD_REQUEST_400; -import static org.eclipse.jetty.http.HttpStatus.NOT_FOUND_404; -import static org.eclipse.jetty.http.HttpStatus.NO_CONTENT_204; -import static org.eclipse.jetty.http.HttpStatus.OK_200; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.Map; -import java.util.stream.Stream; - -import org.apache.james.core.Username; -import org.apache.james.user.api.UsersRepository; -import org.apache.james.user.api.UsersRepositoryException; -import org.apache.james.webadmin.WebAdminServer; -import org.apache.james.webadmin.WebAdminUtils; -import org.apache.james.webadmin.utils.JsonTransformer; -import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import com.linagora.tmail.rate.limiter.api.InMemoryRateLimitingPlanRepository; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanId; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanNotFoundException; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanRepository; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanRepositoryContract; -import com.linagora.tmail.rate.limiter.api.RateLimitingPlanUserRepository; -import com.linagora.tmail.rate.limiter.api.memory.MemoryRateLimitingPlanUserRepository; - -import io.restassured.RestAssured; -import io.restassured.path.json.JsonPath; -import net.javacrumbs.jsonunit.core.Option; -import reactor.core.publisher.Mono; - -public class RateLimitPlanUsersRoutesTest { - private static final String ATTACH_PLAN_TO_USER_PATH = "/users/%s/rate-limit-plans/%s"; - private static final String GET_USERS_OF_PLAN_PATH = "/rate-limit-plans/%s/users"; - private static final String GET_PLAN_OF_USER_PATH = "/users/%s/rate-limit-plans"; - private static final String REVOKE_PLAN_OF_USER_PATH = "/users/%s/rate-limit-plans"; - private static final String VALID_PLAN_ID = "f5f3ce18-504a-4f32-a6f6-3ae41b096dbd"; - public static Username BOB = Username.of("bob@linagora.com"); - public static Username ANDRE = Username.of("andre@linagora.com"); - - private static Stream usernameInvalidSource() { - return Stream.of( - Arguments.of("@"), - Arguments.of("aa@aa@aa") - ); - } - - private WebAdminServer webAdminServer; - private UsersRepository usersRepository; - private RateLimitingPlanUserRepository planUserRepository; - private RateLimitingPlanRepository planRepository; - - @BeforeEach - void setUp() { - planRepository = new InMemoryRateLimitingPlanRepository(); - planUserRepository = new MemoryRateLimitingPlanUserRepository(); - usersRepository = mock(UsersRepository.class); - RateLimitPlanUserRoutes rateLimitPlanUserRoutes = new RateLimitPlanUserRoutes(planUserRepository, planRepository, usersRepository, new JsonTransformer()); - webAdminServer = WebAdminUtils.createWebAdminServer(rateLimitPlanUserRoutes).start(); - - RestAssured.requestSpecification = WebAdminUtils.buildRequestSpecification(webAdminServer).build(); - } - - @AfterEach - void tearDown() { - webAdminServer.destroy(); - } - - @Nested - class AttachPlanToUserTest { - - @ParameterizedTest - @MethodSource("com.linagora.tmail.webadmin.RateLimitPlanUsersRoutesTest#usernameInvalidSource") - void shouldReturnErrorWhenInvalidUser(String username) { - Map errors = given() - .put(String.format(ATTACH_PLAN_TO_USER_PATH, username, VALID_PLAN_ID)) - .then() - .statusCode(BAD_REQUEST_400) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", BAD_REQUEST_400) - .containsEntry("type", "InvalidArgument") - .containsEntry("message", "Invalid arguments supplied in the user request"); - } - - @Test - void shouldReturnNotFoundWhenUserNotFound() { - Map errors = given() - .put(String.format(ATTACH_PLAN_TO_USER_PATH, BOB.asString(), VALID_PLAN_ID)) - .then() - .statusCode(NOT_FOUND_404) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", NOT_FOUND_404) - .containsEntry("type", "notFound") - .containsEntry("message", "User " + BOB.asString() + " does not exist"); - } - - @Test - void shouldReturnErrorWhenInvalidPlanId() { - Map errors = given() - .put(String.format(ATTACH_PLAN_TO_USER_PATH, BOB.asString(), "invalidUUIDString")) - .then() - .statusCode(BAD_REQUEST_400) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", BAD_REQUEST_400) - .containsEntry("type", "InvalidArgument") - .containsEntry("message", "Invalid arguments supplied in the user request") - .containsEntry("details", "Invalid UUID string: invalidUUIDString"); - } - - @Test - void shouldReturnNotFoundWhenPlanDoesNotExist() throws UsersRepositoryException { - when(usersRepository.contains(BOB)).thenReturn(true); - - Map errors = given() - .put(String.format(ATTACH_PLAN_TO_USER_PATH, BOB.asString(), VALID_PLAN_ID)) - .then() - .statusCode(NOT_FOUND_404) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", NOT_FOUND_404) - .containsEntry("type", "notFound") - .containsEntry("message", "Plan id " + VALID_PLAN_ID + " does not exist"); - } - - - @Test - void shouldSucceedWhenPlanExists() throws UsersRepositoryException { - when(usersRepository.contains(BOB)).thenReturn(true); - RateLimitingPlanId planId = Mono.from(planRepository.create(RateLimitingPlanRepositoryContract.CREATION_REQUEST())).block().id(); - - String response = given() - .put(String.format(ATTACH_PLAN_TO_USER_PATH, BOB.asString(), planId.serialize())) - .then() - .statusCode(NO_CONTENT_204) - .contentType(JSON) - .extract() - .body() - .asString(); - - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(response).isEmpty(); - softly.assertThat(Mono.from(planUserRepository.getPlanByUser(BOB)).block()).isEqualTo(planId); - }); - } - - @Test - void shouldOverridePreviousPlanWithNewPlan() throws UsersRepositoryException { - when(usersRepository.contains(BOB)).thenReturn(true); - RateLimitingPlanId previousPlanId = Mono.from(planRepository.create(RateLimitingPlanRepositoryContract.CREATION_REQUEST())).block().id(); - RateLimitingPlanId newPlanId = Mono.from(planRepository.create(RateLimitingPlanRepositoryContract.CREATION_REQUEST())).block().id(); - - given() - .put(String.format(ATTACH_PLAN_TO_USER_PATH, BOB.asString(), previousPlanId.serialize())) - .then() - .statusCode(NO_CONTENT_204) - .contentType(JSON) - .extract() - .body() - .asString(); - - String response = given() - .put(String.format(ATTACH_PLAN_TO_USER_PATH, BOB.asString(), newPlanId.serialize())) - .then() - .statusCode(NO_CONTENT_204) - .contentType(JSON) - .extract() - .body() - .asString(); - - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(response).isEmpty(); - softly.assertThat(Mono.from(planUserRepository.getPlanByUser(BOB)).block()).isEqualTo(newPlanId); - }); - } - - @Test - void shouldBeIdempotent() throws UsersRepositoryException { - when(usersRepository.contains(BOB)).thenReturn(true); - RateLimitingPlanId planId = Mono.from(planRepository.create(RateLimitingPlanRepositoryContract.CREATION_REQUEST())).block().id(); - - given() - .put(String.format(ATTACH_PLAN_TO_USER_PATH, BOB.asString(), planId.serialize())) - .then() - .statusCode(NO_CONTENT_204) - .contentType(JSON) - .extract() - .body() - .asString(); - - String response = given() - .put(String.format(ATTACH_PLAN_TO_USER_PATH, BOB.asString(), planId.serialize())) - .then() - .statusCode(NO_CONTENT_204) - .contentType(JSON) - .extract() - .body() - .asString(); - - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(response).isEmpty(); - softly.assertThat(Mono.from(planUserRepository.getPlanByUser(BOB)).block()).isEqualTo(planId); - }); - } - } - - @Nested - class GetUsersOfPlan { - - @Test - void shouldReturnErrorWhenInvalidPlanId() { - Map errors = given() - .get(String.format(GET_USERS_OF_PLAN_PATH, "invalidUUIDString")) - .then() - .statusCode(BAD_REQUEST_400) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", BAD_REQUEST_400) - .containsEntry("type", "InvalidArgument") - .containsEntry("message", "Invalid arguments supplied in the user request") - .containsEntry("details", "Invalid UUID string: invalidUUIDString"); - } - - @Test - void shouldReturnNotFoundWhenPlanDoesNotExist() { - Map errors = given() - .get(String.format(GET_USERS_OF_PLAN_PATH, VALID_PLAN_ID)) - .then() - .statusCode(NOT_FOUND_404) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", NOT_FOUND_404) - .containsEntry("type", "notFound") - .containsEntry("message", "Plan id " + VALID_PLAN_ID + " does not exist"); - } - - - @Test - void shouldReturnUsersWhenPlanExistsAndHasUsersAttached() throws UsersRepositoryException { - when(usersRepository.contains(BOB)).thenReturn(true); - RateLimitingPlanId planId = Mono.from(planRepository.create(RateLimitingPlanRepositoryContract.CREATION_REQUEST())).block().id(); - Mono.from(planUserRepository.applyPlan(BOB, planId)).block(); - Mono.from(planUserRepository.applyPlan(ANDRE, planId)).block(); - - String response = given() - .get(String.format(GET_USERS_OF_PLAN_PATH, planId.serialize())) - .then() - .statusCode(OK_200) - .contentType(JSON) - .extract() - .body() - .asString(); - - assertThatJson(response) - .when(Option.IGNORING_ARRAY_ORDER) - .isEqualTo("[\"bob@linagora.com\", \"andre@linagora.com\"]"); - } - - @Test - void shouldReturnEmptyWhenPlanExistsAndHasNonUsersAttached() throws UsersRepositoryException { - when(usersRepository.contains(BOB)).thenReturn(true); - RateLimitingPlanId planId = Mono.from(planRepository.create(RateLimitingPlanRepositoryContract.CREATION_REQUEST())).block().id(); - - String response = given() - .get(String.format(GET_USERS_OF_PLAN_PATH, planId.serialize())) - .then() - .statusCode(OK_200) - .contentType(JSON) - .extract() - .body() - .asString(); - - assertThatJson(response) - .isEqualTo("[]"); - } - } - - @Nested - class GetPlanOfUserTest { - - @ParameterizedTest - @MethodSource("com.linagora.tmail.webadmin.RateLimitPlanUsersRoutesTest#usernameInvalidSource") - void shouldReturnErrorWhenInvalidUser(String username) { - Map errors = given() - .get(String.format(GET_PLAN_OF_USER_PATH, username)) - .then() - .statusCode(BAD_REQUEST_400) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", BAD_REQUEST_400) - .containsEntry("type", "InvalidArgument") - .containsEntry("message", "Invalid arguments supplied in the user request"); - } - - @Test - void shouldReturnNotFoundWhenUserNotFound() { - Map errors = given() - .get(String.format(GET_PLAN_OF_USER_PATH, BOB.asString())) - .then() - .statusCode(NOT_FOUND_404) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", NOT_FOUND_404) - .containsEntry("type", "notFound") - .containsEntry("message", "User " + BOB.asString() + " does not exist"); - } - - @Test - void shouldReturnNotFoundWhenUserDoesNotHaveAPlan() throws UsersRepositoryException { - when(usersRepository.contains(BOB)).thenReturn(true); - - Map errors = given() - .get(String.format(GET_PLAN_OF_USER_PATH, BOB.asString())) - .then() - .statusCode(NOT_FOUND_404) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", NOT_FOUND_404) - .containsEntry("type", "notFound") - .containsEntry("message", "User " + BOB.asString() + " does not have a plan"); - } - - @Test - void shouldReturnPlanIdWhenUserHasAPlan() throws UsersRepositoryException { - when(usersRepository.contains(BOB)).thenReturn(true); - RateLimitingPlanId planId = Mono.from(planRepository.create(RateLimitingPlanRepositoryContract.CREATION_REQUEST())).block().id(); - Mono.from(planUserRepository.applyPlan(BOB, planId)).block(); - - JsonPath jsonPath = given() - .get(String.format(GET_PLAN_OF_USER_PATH, BOB.asString())) - .then() - .statusCode(OK_200) - .contentType(JSON) - .extract() - .body() - .jsonPath(); - - assertThat(jsonPath.getString("planId")).isEqualTo(planId.serialize()); - } - } - - @Nested - class RevokePlanOfUser { - - @ParameterizedTest - @MethodSource("com.linagora.tmail.webadmin.RateLimitPlanUsersRoutesTest#usernameInvalidSource") - void shouldReturnErrorWhenInvalidUser(String username) { - Map errors = given() - .delete(String.format(REVOKE_PLAN_OF_USER_PATH, username)) - .then() - .statusCode(BAD_REQUEST_400) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", BAD_REQUEST_400) - .containsEntry("type", "InvalidArgument") - .containsEntry("message", "Invalid arguments supplied in the user request"); - } - - @Test - void shouldReturnNotFoundWhenUserNotFound() { - Map errors = given() - .delete(String.format(REVOKE_PLAN_OF_USER_PATH, BOB.asString())) - .then() - .statusCode(NOT_FOUND_404) - .contentType(JSON) - .extract() - .body() - .jsonPath() - .getMap("."); - - assertThat(errors) - .containsEntry("statusCode", NOT_FOUND_404) - .containsEntry("type", "notFound") - .containsEntry("message", "User " + BOB.asString() + " does not exist"); - } - - @Test - void shouldRevokePlanOfUser() throws UsersRepositoryException { - when(usersRepository.contains(BOB)).thenReturn(true); - RateLimitingPlanId planId = Mono.from(planRepository.create(RateLimitingPlanRepositoryContract.CREATION_REQUEST())).block().id(); - Mono.from(planUserRepository.applyPlan(BOB, planId)).block(); - - String response = given() - .delete(String.format(REVOKE_PLAN_OF_USER_PATH, BOB.asString())) - .then() - .statusCode(NO_CONTENT_204) - .contentType(JSON) - .extract() - .body() - .asString(); - - assertThat(response).isEmpty(); - assertThatThrownBy(() -> Mono.from(planUserRepository.getPlanByUser(BOB)).block()) - .isInstanceOf(RateLimitingPlanNotFoundException.class); - } - - @Test - void shouldBeIdempotent() throws UsersRepositoryException { - when(usersRepository.contains(BOB)).thenReturn(true); - RateLimitingPlanId planId = Mono.from(planRepository.create(RateLimitingPlanRepositoryContract.CREATION_REQUEST())).block().id(); - Mono.from(planUserRepository.applyPlan(BOB, planId)).block(); - - given() - .delete(String.format(REVOKE_PLAN_OF_USER_PATH, BOB.asString())) - .then() - .statusCode(NO_CONTENT_204) - .contentType(JSON) - .extract() - .body() - .asString(); - - String response = given() - .delete(String.format(REVOKE_PLAN_OF_USER_PATH, BOB.asString())) - .then() - .statusCode(NO_CONTENT_204) - .contentType(JSON) - .extract() - .body() - .asString(); - - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(response).isEmpty(); - softly.assertThatThrownBy(() -> Mono.from(planUserRepository.getPlanByUser(BOB)).block()) - .isInstanceOf(RateLimitingPlanNotFoundException.class); - }); - } - } -}