From 5acfcd74a14d0feac4f885c9bf8a9584b3cfc6e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mandresy=20Ga=C3=ABtan=20RANDRIANANTOANINA?= Date: Thu, 9 Jul 2026 23:09:37 +0300 Subject: [PATCH 01/13] feat: add FeesOnly annotation for fee-only mode --- src/main/java/school/hei/haapi/endpoint/FeesOnly.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/java/school/hei/haapi/endpoint/FeesOnly.java diff --git a/src/main/java/school/hei/haapi/endpoint/FeesOnly.java b/src/main/java/school/hei/haapi/endpoint/FeesOnly.java new file mode 100644 index 000000000..837dddb44 --- /dev/null +++ b/src/main/java/school/hei/haapi/endpoint/FeesOnly.java @@ -0,0 +1,8 @@ +package school.hei.haapi.endpoint; + +import java.lang.annotation.*; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface FeesOnly { +} \ No newline at end of file From e5fb6551dfb11cd5efa437ca6b11ba6a1f8e5d35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mandresy=20Ga=C3=ABtan=20RANDRIANANTOANINA?= Date: Thu, 9 Jul 2026 23:12:08 +0300 Subject: [PATCH 02/13] feat: add FeesOnly interceptor for fee-only mode --- .../haapi/endpoint/FeesOnlyInterceptor.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/main/java/school/hei/haapi/endpoint/FeesOnlyInterceptor.java diff --git a/src/main/java/school/hei/haapi/endpoint/FeesOnlyInterceptor.java b/src/main/java/school/hei/haapi/endpoint/FeesOnlyInterceptor.java new file mode 100644 index 000000000..d92bc6259 --- /dev/null +++ b/src/main/java/school/hei/haapi/endpoint/FeesOnlyInterceptor.java @@ -0,0 +1,57 @@ +package school.hei.haapi.endpoint; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +@Slf4j +@Component +public class FeesOnlyInterceptor implements HandlerInterceptor { + private final boolean feesOnly; + public FeesOnlyInterceptor(@Value("${FEES_ONLY:false}") boolean feesOnly) { + this.feesOnly = feesOnly; + } + @Override + public boolean preHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler) throws Exception { + + String uri = request.getRequestURI(); + + if (!feesOnly || isAlwaysAllowed(uri)) {return true;} + if (!(handler instanceof HandlerMethod handlerMethod)) {return true;} + boolean allowed = + handlerMethod.getBeanType().isAnnotationPresent(FeesOnly.class) + || handlerMethod.getMethod().isAnnotationPresent(FeesOnly.class); + if (!allowed) { + log.warn("Blocked by FEES_ONLY: {} {}", request.getMethod(), uri); + response.sendError( + HttpServletResponse.SC_FORBIDDEN, + "FEES_ONLY mode active"); + return false; + } + return true; + } + private boolean isAlwaysAllowed(String uri) { + return uri.equals("/whoami") + || uri.equals("/ping") + || uri.equals("/health/db") + || uri.startsWith("/authentication/") + || uri.startsWith("/oauth2/") + || uri.startsWith("/oauth2/authorization/") + || uri.startsWith("/login") + || uri.startsWith("/login/oauth2/") + || uri.startsWith("/logout") + || uri.startsWith("/error") + || uri.startsWith("/actuator") + || uri.startsWith("/auth/") + || uri.startsWith("/api/auth/") + || uri.startsWith("/callback") + || uri.startsWith("/casdoor/"); + } +} \ No newline at end of file From 6a9acda59ea7ae3ac052969cbfc17a091abb9061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mandresy=20Ga=C3=ABtan=20RANDRIANANTOANINA?= Date: Thu, 9 Jul 2026 23:14:51 +0300 Subject: [PATCH 03/13] feat: register FeesOnly interceptor in Spring MVC --- .../FeesOnlyInterceptorConfigurer.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/main/java/school/hei/haapi/endpoint/FeesOnlyInterceptorConfigurer.java diff --git a/src/main/java/school/hei/haapi/endpoint/FeesOnlyInterceptorConfigurer.java b/src/main/java/school/hei/haapi/endpoint/FeesOnlyInterceptorConfigurer.java new file mode 100644 index 000000000..4d21528de --- /dev/null +++ b/src/main/java/school/hei/haapi/endpoint/FeesOnlyInterceptorConfigurer.java @@ -0,0 +1,19 @@ +package school.hei.haapi.endpoint; + +import lombok.AllArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@AllArgsConstructor +public class FeesOnlyInterceptorConfigurer implements WebMvcConfigurer { + + private final FeesOnlyInterceptor feesOnlyInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(feesOnlyInterceptor) + .addPathPatterns("/**"); + } +} \ No newline at end of file From fd71f2181a27840d58c4be3734f565f9d8eaa977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mandresy=20Ga=C3=ABtan=20RANDRIANANTOANINA?= Date: Thu, 9 Jul 2026 23:20:06 +0300 Subject: [PATCH 04/13] feat: annotate necessary controllers for fee-only mode --- .../hei/haapi/endpoint/rest/controller/AdminController.java | 2 ++ .../hei/haapi/endpoint/rest/controller/FeeController.java | 2 ++ .../hei/haapi/endpoint/rest/controller/ManagerController.java | 2 ++ .../hei/haapi/endpoint/rest/controller/PaymentController.java | 2 ++ .../hei/haapi/endpoint/rest/controller/StudentController.java | 2 ++ 5 files changed, 10 insertions(+) diff --git a/src/main/java/school/hei/haapi/endpoint/rest/controller/AdminController.java b/src/main/java/school/hei/haapi/endpoint/rest/controller/AdminController.java index f1c0c4949..815a2d1ab 100644 --- a/src/main/java/school/hei/haapi/endpoint/rest/controller/AdminController.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/controller/AdminController.java @@ -5,6 +5,7 @@ import lombok.AllArgsConstructor; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import school.hei.haapi.endpoint.FeesOnly; import school.hei.haapi.endpoint.rest.mapper.UserMapper; import school.hei.haapi.endpoint.rest.model.Admin; import school.hei.haapi.endpoint.rest.model.CrupdateManager; @@ -13,6 +14,7 @@ @RestController @AllArgsConstructor +@FeesOnly public class AdminController { private final UserService userService; diff --git a/src/main/java/school/hei/haapi/endpoint/rest/controller/FeeController.java b/src/main/java/school/hei/haapi/endpoint/rest/controller/FeeController.java index ecaa43fcc..c9a211ba4 100644 --- a/src/main/java/school/hei/haapi/endpoint/rest/controller/FeeController.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/controller/FeeController.java @@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import school.hei.haapi.endpoint.FeesOnly; import school.hei.haapi.endpoint.rest.mapper.FeeMapper; import school.hei.haapi.endpoint.rest.mapper.FeeTemplateMapper; import school.hei.haapi.endpoint.rest.model.*; @@ -37,6 +38,7 @@ @RestController @AllArgsConstructor +@FeesOnly @Slf4j public class FeeController { private final UserService userService; diff --git a/src/main/java/school/hei/haapi/endpoint/rest/controller/ManagerController.java b/src/main/java/school/hei/haapi/endpoint/rest/controller/ManagerController.java index 10eca5a12..2d6583a3a 100644 --- a/src/main/java/school/hei/haapi/endpoint/rest/controller/ManagerController.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/controller/ManagerController.java @@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import school.hei.haapi.endpoint.FeesOnly; import school.hei.haapi.endpoint.rest.mapper.SexEnumMapper; import school.hei.haapi.endpoint.rest.mapper.StatusEnumMapper; import school.hei.haapi.endpoint.rest.mapper.UserMapper; @@ -30,6 +31,7 @@ @RestController @AllArgsConstructor +@FeesOnly public class ManagerController { private final SexEnumMapper sexEnumMapper; private final StatusEnumMapper statusEnumMapper; diff --git a/src/main/java/school/hei/haapi/endpoint/rest/controller/PaymentController.java b/src/main/java/school/hei/haapi/endpoint/rest/controller/PaymentController.java index 7181b72d5..aaea7ed2e 100644 --- a/src/main/java/school/hei/haapi/endpoint/rest/controller/PaymentController.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/controller/PaymentController.java @@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import school.hei.haapi.endpoint.FeesOnly; import school.hei.haapi.endpoint.rest.mapper.PaymentMapper; import school.hei.haapi.endpoint.rest.model.CreatePayment; import school.hei.haapi.endpoint.rest.model.Payment; @@ -21,6 +22,7 @@ @RestController @AllArgsConstructor +@FeesOnly public class PaymentController { private final PaymentService paymentService; private final PaymentMapper paymentMapper; diff --git a/src/main/java/school/hei/haapi/endpoint/rest/controller/StudentController.java b/src/main/java/school/hei/haapi/endpoint/rest/controller/StudentController.java index 0ba5cad80..49818eda9 100644 --- a/src/main/java/school/hei/haapi/endpoint/rest/controller/StudentController.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/controller/StudentController.java @@ -17,6 +17,7 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import school.hei.haapi.endpoint.FeesOnly; import school.hei.haapi.endpoint.rest.mapper.GroupFlowMapper; import school.hei.haapi.endpoint.rest.mapper.SexEnumMapper; import school.hei.haapi.endpoint.rest.mapper.StatusCheckMapper; @@ -50,6 +51,7 @@ @RestController @AllArgsConstructor +@FeesOnly public class StudentController { private final UserService userService; private final UserMapper userMapper; From f5b84e225c5b9d4380632f9214e86620163e027f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mandresy=20Ga=C3=ABtan=20RANDRIANANTOANINA?= Date: Tue, 14 Jul 2026 12:52:04 +0300 Subject: [PATCH 05/13] feat: add tests for FeesOnly features --- .../school/hei/haapi/FeesOnlyFalseIT.java | 53 ++++++++++++++++ .../java/school/hei/haapi/FeesOnlyTrueIT.java | 60 +++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 src/test/java/school/hei/haapi/FeesOnlyFalseIT.java create mode 100644 src/test/java/school/hei/haapi/FeesOnlyTrueIT.java diff --git a/src/test/java/school/hei/haapi/FeesOnlyFalseIT.java b/src/test/java/school/hei/haapi/FeesOnlyFalseIT.java new file mode 100644 index 000000000..1980d2f03 --- /dev/null +++ b/src/test/java/school/hei/haapi/FeesOnlyFalseIT.java @@ -0,0 +1,53 @@ +package school.hei.haapi; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static school.hei.haapi.integration.conf.TestUtils.MANAGER1_TOKEN; +import static school.hei.haapi.integration.conf.TestUtils.setUpCasdoor; +import static school.hei.haapi.integration.conf.TestUtils.setUpCognito; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.test.context.TestPropertySource; +import org.testcontainers.junit.jupiter.Testcontainers; +import school.hei.haapi.endpoint.rest.api.PayingApi; +import school.hei.haapi.endpoint.rest.api.UsersApi; +import school.hei.haapi.endpoint.rest.client.ApiClient; +import school.hei.haapi.integration.conf.FacadeITMockedThirdParties; +import school.hei.haapi.integration.conf.TestUtils; + +/** + * Vérifie que lorsque la variable d'environnement FEES_ONLY vaut "false" (comportement par + * défaut), un utilisateur autorisé a accès à tous les endpoints, qu'ils soient annotés + * {@code @FeesOnly} (ex: /fees) ou non (ex: /monitors). + */ +@Testcontainers +@AutoConfigureMockMvc +@TestPropertySource(properties = "FEES_ONLY=false") +class FeesOnlyDisabledIT extends FacadeITMockedThirdParties { + + private ApiClient anApiClient(String token) { + return TestUtils.anApiClient(token, localPort); + } + + @BeforeEach + void setUp() { + setUpCasdoor(casdoorAuthServiceMock, certificateLoaderMock); + setUpCognito(cognitoComponentMock); + } + + @Test + void fees_only_endpoint_is_accessible_when_fees_only_disabled() { + var api = new PayingApi(anApiClient(MANAGER1_TOKEN)); + + assertDoesNotThrow( + () -> api.getFees(null, null, null, null, null, null, 1, 10, false, null)); + } + + @Test + void non_fees_only_endpoint_is_accessible_when_fees_only_disabled() { + var api = new UsersApi(anApiClient(MANAGER1_TOKEN)); + + assertDoesNotThrow(() -> api.getMonitors(1, 10, null, null, null)); + } +} \ No newline at end of file diff --git a/src/test/java/school/hei/haapi/FeesOnlyTrueIT.java b/src/test/java/school/hei/haapi/FeesOnlyTrueIT.java new file mode 100644 index 000000000..fbf19c686 --- /dev/null +++ b/src/test/java/school/hei/haapi/FeesOnlyTrueIT.java @@ -0,0 +1,60 @@ +package school.hei.haapi; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static school.hei.haapi.integration.conf.TestUtils.MANAGER1_TOKEN; +import static school.hei.haapi.integration.conf.TestUtils.setUpCasdoor; +import static school.hei.haapi.integration.conf.TestUtils.setUpCognito; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.test.context.TestPropertySource; +import org.testcontainers.junit.jupiter.Testcontainers; +import school.hei.haapi.endpoint.rest.api.PayingApi; +import school.hei.haapi.endpoint.rest.api.UsersApi; +import school.hei.haapi.endpoint.rest.client.ApiClient; +import school.hei.haapi.endpoint.rest.client.ApiException; +import school.hei.haapi.integration.conf.FacadeITMockedThirdParties; +import school.hei.haapi.integration.conf.TestUtils; + +/** + * Vérifie que lorsque la variable d'environnement FEES_ONLY vaut "true", seuls les endpoints + * annotés {@code @FeesOnly} (ex: /fees, porté par FeeController) restent accessibles, tandis que + * les endpoints non annotés (ex: /monitors, porté par MonitorController) sont bloqués avec un + * statut 403. + */ +@Testcontainers +@AutoConfigureMockMvc +@TestPropertySource(properties = "FEES_ONLY=true") +class FeesOnlyEnabledIT extends FacadeITMockedThirdParties { + + private ApiClient anApiClient(String token) { + return TestUtils.anApiClient(token, localPort); + } + + @BeforeEach + void setUp() { + setUpCasdoor(casdoorAuthServiceMock, certificateLoaderMock); + setUpCognito(cognitoComponentMock); + } + + @Test + void fees_only_endpoint_stays_accessible_when_fees_only_enabled() { + var api = new PayingApi(anApiClient(MANAGER1_TOKEN)); + + assertDoesNotThrow( + () -> api.getFees(null, null, null, null, null, null, 1, 10, false, null)); + } + + @Test + void non_fees_only_endpoint_is_blocked_when_fees_only_enabled() { + var api = new UsersApi(anApiClient(MANAGER1_TOKEN)); + + ApiException exception = + assertThrows(ApiException.class, () -> api.getMonitors(1, 10, null, null, null)); + + assertEquals(403, exception.getCode()); + } +} \ No newline at end of file From 419b89f541aa99cb0012d3dd447e76460e1292cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mandresy=20Ga=C3=ABtan=20RANDRIANANTOANINA?= Date: Fri, 17 Jul 2026 11:51:58 +0300 Subject: [PATCH 06/13] chore: modify some variables type to var for less verbosity && remove useless comment in FeesOnlytest --- .../school/hei/haapi/endpoint/FeesOnlyInterceptor.java | 4 ++-- src/test/java/school/hei/haapi/FeesOnlyFalseIT.java | 5 ----- src/test/java/school/hei/haapi/FeesOnlyTrueIT.java | 8 +------- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/main/java/school/hei/haapi/endpoint/FeesOnlyInterceptor.java b/src/main/java/school/hei/haapi/endpoint/FeesOnlyInterceptor.java index d92bc6259..63acbe3fa 100644 --- a/src/main/java/school/hei/haapi/endpoint/FeesOnlyInterceptor.java +++ b/src/main/java/school/hei/haapi/endpoint/FeesOnlyInterceptor.java @@ -21,11 +21,11 @@ public boolean preHandle( HttpServletResponse response, Object handler) throws Exception { - String uri = request.getRequestURI(); + var uri = request.getRequestURI(); if (!feesOnly || isAlwaysAllowed(uri)) {return true;} if (!(handler instanceof HandlerMethod handlerMethod)) {return true;} - boolean allowed = + var allowed = handlerMethod.getBeanType().isAnnotationPresent(FeesOnly.class) || handlerMethod.getMethod().isAnnotationPresent(FeesOnly.class); if (!allowed) { diff --git a/src/test/java/school/hei/haapi/FeesOnlyFalseIT.java b/src/test/java/school/hei/haapi/FeesOnlyFalseIT.java index 1980d2f03..a297972b7 100644 --- a/src/test/java/school/hei/haapi/FeesOnlyFalseIT.java +++ b/src/test/java/school/hei/haapi/FeesOnlyFalseIT.java @@ -16,11 +16,6 @@ import school.hei.haapi.integration.conf.FacadeITMockedThirdParties; import school.hei.haapi.integration.conf.TestUtils; -/** - * Vérifie que lorsque la variable d'environnement FEES_ONLY vaut "false" (comportement par - * défaut), un utilisateur autorisé a accès à tous les endpoints, qu'ils soient annotés - * {@code @FeesOnly} (ex: /fees) ou non (ex: /monitors). - */ @Testcontainers @AutoConfigureMockMvc @TestPropertySource(properties = "FEES_ONLY=false") diff --git a/src/test/java/school/hei/haapi/FeesOnlyTrueIT.java b/src/test/java/school/hei/haapi/FeesOnlyTrueIT.java index fbf19c686..cf242a492 100644 --- a/src/test/java/school/hei/haapi/FeesOnlyTrueIT.java +++ b/src/test/java/school/hei/haapi/FeesOnlyTrueIT.java @@ -19,12 +19,6 @@ import school.hei.haapi.integration.conf.FacadeITMockedThirdParties; import school.hei.haapi.integration.conf.TestUtils; -/** - * Vérifie que lorsque la variable d'environnement FEES_ONLY vaut "true", seuls les endpoints - * annotés {@code @FeesOnly} (ex: /fees, porté par FeeController) restent accessibles, tandis que - * les endpoints non annotés (ex: /monitors, porté par MonitorController) sont bloqués avec un - * statut 403. - */ @Testcontainers @AutoConfigureMockMvc @TestPropertySource(properties = "FEES_ONLY=true") @@ -52,7 +46,7 @@ void fees_only_endpoint_stays_accessible_when_fees_only_enabled() { void non_fees_only_endpoint_is_blocked_when_fees_only_enabled() { var api = new UsersApi(anApiClient(MANAGER1_TOKEN)); - ApiException exception = + var exception = assertThrows(ApiException.class, () -> api.getMonitors(1, 10, null, null, null)); assertEquals(403, exception.getCode()); From 972e5713ddfb0b38a71361b96d88d8732e63406b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mandresy=20Ga=C3=ABtan=20RANDRIANANTOANINA?= Date: Sun, 19 Jul 2026 00:31:22 +0300 Subject: [PATCH 07/13] refactor: centralize feesOnly rules in SecurityFilterChain for maintainability --- .../school/hei/haapi/endpoint/FeesOnly.java | 8 --- .../haapi/endpoint/FeesOnlyInterceptor.java | 57 ------------------- .../FeesOnlyInterceptorConfigurer.java | 19 ------- .../rest/controller/AdminController.java | 2 - .../rest/controller/FeeController.java | 2 - .../rest/controller/ManagerController.java | 2 - .../rest/controller/PaymentController.java | 2 - .../rest/controller/StudentController.java | 2 - .../endpoint/rest/security/SecurityConf.java | 42 +++++++++++++- 9 files changed, 41 insertions(+), 95 deletions(-) delete mode 100644 src/main/java/school/hei/haapi/endpoint/FeesOnly.java delete mode 100644 src/main/java/school/hei/haapi/endpoint/FeesOnlyInterceptor.java delete mode 100644 src/main/java/school/hei/haapi/endpoint/FeesOnlyInterceptorConfigurer.java diff --git a/src/main/java/school/hei/haapi/endpoint/FeesOnly.java b/src/main/java/school/hei/haapi/endpoint/FeesOnly.java deleted file mode 100644 index 837dddb44..000000000 --- a/src/main/java/school/hei/haapi/endpoint/FeesOnly.java +++ /dev/null @@ -1,8 +0,0 @@ -package school.hei.haapi.endpoint; - -import java.lang.annotation.*; - -@Target({ElementType.TYPE, ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface FeesOnly { -} \ No newline at end of file diff --git a/src/main/java/school/hei/haapi/endpoint/FeesOnlyInterceptor.java b/src/main/java/school/hei/haapi/endpoint/FeesOnlyInterceptor.java deleted file mode 100644 index 63acbe3fa..000000000 --- a/src/main/java/school/hei/haapi/endpoint/FeesOnlyInterceptor.java +++ /dev/null @@ -1,57 +0,0 @@ -package school.hei.haapi.endpoint; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import org.springframework.web.method.HandlerMethod; -import org.springframework.web.servlet.HandlerInterceptor; - -@Slf4j -@Component -public class FeesOnlyInterceptor implements HandlerInterceptor { - private final boolean feesOnly; - public FeesOnlyInterceptor(@Value("${FEES_ONLY:false}") boolean feesOnly) { - this.feesOnly = feesOnly; - } - @Override - public boolean preHandle( - HttpServletRequest request, - HttpServletResponse response, - Object handler) throws Exception { - - var uri = request.getRequestURI(); - - if (!feesOnly || isAlwaysAllowed(uri)) {return true;} - if (!(handler instanceof HandlerMethod handlerMethod)) {return true;} - var allowed = - handlerMethod.getBeanType().isAnnotationPresent(FeesOnly.class) - || handlerMethod.getMethod().isAnnotationPresent(FeesOnly.class); - if (!allowed) { - log.warn("Blocked by FEES_ONLY: {} {}", request.getMethod(), uri); - response.sendError( - HttpServletResponse.SC_FORBIDDEN, - "FEES_ONLY mode active"); - return false; - } - return true; - } - private boolean isAlwaysAllowed(String uri) { - return uri.equals("/whoami") - || uri.equals("/ping") - || uri.equals("/health/db") - || uri.startsWith("/authentication/") - || uri.startsWith("/oauth2/") - || uri.startsWith("/oauth2/authorization/") - || uri.startsWith("/login") - || uri.startsWith("/login/oauth2/") - || uri.startsWith("/logout") - || uri.startsWith("/error") - || uri.startsWith("/actuator") - || uri.startsWith("/auth/") - || uri.startsWith("/api/auth/") - || uri.startsWith("/callback") - || uri.startsWith("/casdoor/"); - } -} \ No newline at end of file diff --git a/src/main/java/school/hei/haapi/endpoint/FeesOnlyInterceptorConfigurer.java b/src/main/java/school/hei/haapi/endpoint/FeesOnlyInterceptorConfigurer.java deleted file mode 100644 index 4d21528de..000000000 --- a/src/main/java/school/hei/haapi/endpoint/FeesOnlyInterceptorConfigurer.java +++ /dev/null @@ -1,19 +0,0 @@ -package school.hei.haapi.endpoint; - -import lombok.AllArgsConstructor; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -@AllArgsConstructor -public class FeesOnlyInterceptorConfigurer implements WebMvcConfigurer { - - private final FeesOnlyInterceptor feesOnlyInterceptor; - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(feesOnlyInterceptor) - .addPathPatterns("/**"); - } -} \ No newline at end of file diff --git a/src/main/java/school/hei/haapi/endpoint/rest/controller/AdminController.java b/src/main/java/school/hei/haapi/endpoint/rest/controller/AdminController.java index 815a2d1ab..f1c0c4949 100644 --- a/src/main/java/school/hei/haapi/endpoint/rest/controller/AdminController.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/controller/AdminController.java @@ -5,7 +5,6 @@ import lombok.AllArgsConstructor; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -import school.hei.haapi.endpoint.FeesOnly; import school.hei.haapi.endpoint.rest.mapper.UserMapper; import school.hei.haapi.endpoint.rest.model.Admin; import school.hei.haapi.endpoint.rest.model.CrupdateManager; @@ -14,7 +13,6 @@ @RestController @AllArgsConstructor -@FeesOnly public class AdminController { private final UserService userService; diff --git a/src/main/java/school/hei/haapi/endpoint/rest/controller/FeeController.java b/src/main/java/school/hei/haapi/endpoint/rest/controller/FeeController.java index 65cf037db..e695d2307 100644 --- a/src/main/java/school/hei/haapi/endpoint/rest/controller/FeeController.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/controller/FeeController.java @@ -20,7 +20,6 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import school.hei.haapi.endpoint.FeesOnly; import school.hei.haapi.endpoint.rest.mapper.FeeMapper; import school.hei.haapi.endpoint.rest.mapper.FeeTemplateMapper; import school.hei.haapi.endpoint.rest.model.*; @@ -39,7 +38,6 @@ @RestController @AllArgsConstructor -@FeesOnly @Slf4j @TrackActivity public class FeeController { diff --git a/src/main/java/school/hei/haapi/endpoint/rest/controller/ManagerController.java b/src/main/java/school/hei/haapi/endpoint/rest/controller/ManagerController.java index 2d6583a3a..10eca5a12 100644 --- a/src/main/java/school/hei/haapi/endpoint/rest/controller/ManagerController.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/controller/ManagerController.java @@ -14,7 +14,6 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import school.hei.haapi.endpoint.FeesOnly; import school.hei.haapi.endpoint.rest.mapper.SexEnumMapper; import school.hei.haapi.endpoint.rest.mapper.StatusEnumMapper; import school.hei.haapi.endpoint.rest.mapper.UserMapper; @@ -31,7 +30,6 @@ @RestController @AllArgsConstructor -@FeesOnly public class ManagerController { private final SexEnumMapper sexEnumMapper; private final StatusEnumMapper statusEnumMapper; diff --git a/src/main/java/school/hei/haapi/endpoint/rest/controller/PaymentController.java b/src/main/java/school/hei/haapi/endpoint/rest/controller/PaymentController.java index aaea7ed2e..7181b72d5 100644 --- a/src/main/java/school/hei/haapi/endpoint/rest/controller/PaymentController.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/controller/PaymentController.java @@ -11,7 +11,6 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import school.hei.haapi.endpoint.FeesOnly; import school.hei.haapi.endpoint.rest.mapper.PaymentMapper; import school.hei.haapi.endpoint.rest.model.CreatePayment; import school.hei.haapi.endpoint.rest.model.Payment; @@ -22,7 +21,6 @@ @RestController @AllArgsConstructor -@FeesOnly public class PaymentController { private final PaymentService paymentService; private final PaymentMapper paymentMapper; diff --git a/src/main/java/school/hei/haapi/endpoint/rest/controller/StudentController.java b/src/main/java/school/hei/haapi/endpoint/rest/controller/StudentController.java index 49818eda9..0ba5cad80 100644 --- a/src/main/java/school/hei/haapi/endpoint/rest/controller/StudentController.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/controller/StudentController.java @@ -17,7 +17,6 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import school.hei.haapi.endpoint.FeesOnly; import school.hei.haapi.endpoint.rest.mapper.GroupFlowMapper; import school.hei.haapi.endpoint.rest.mapper.SexEnumMapper; import school.hei.haapi.endpoint.rest.mapper.StatusCheckMapper; @@ -51,7 +50,6 @@ @RestController @AllArgsConstructor -@FeesOnly public class StudentController { private final UserService userService; private final UserMapper userMapper; diff --git a/src/main/java/school/hei/haapi/endpoint/rest/security/SecurityConf.java b/src/main/java/school/hei/haapi/endpoint/rest/security/SecurityConf.java index 74354e7fa..96d35a73f 100644 --- a/src/main/java/school/hei/haapi/endpoint/rest/security/SecurityConf.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/security/SecurityConf.java @@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider; @@ -36,6 +37,7 @@ import school.hei.haapi.repository.CorRepository; import school.hei.haapi.service.CourseAssignmentService; import school.hei.haapi.service.MonitoringStudentService; +import org.springframework.beans.factory.annotation.Value; @Configuration @Slf4j @@ -48,6 +50,9 @@ public class SecurityConf { private final HandlerExceptionResolver exceptionResolver; private final CorRepository corRepository; + @Value("${FEES_ONLY:false}") + private boolean feesOnly; + public SecurityConf( CasdoorAuthProvider authProvider, // InternalToExternalErrorHandler behind @@ -67,7 +72,28 @@ public AuthenticationManager authenticationManager() { return new ProviderManager(authProvider); } - @Bean + @Bean + @Order(1) + public SecurityFilterChain feesOnlyFilterChain(HttpSecurity http) throws Exception { + if (!feesOnly) { + http + .securityMatcher(request -> false) + .authorizeHttpRequests(req -> req.anyRequest().denyAll()); + return http.build(); + } + + http + .securityMatcher(request -> !isFeesOnlyAllowed(request.getRequestURI())) + .authorizeHttpRequests(req -> req.anyRequest().denyAll()) + .cors(AbstractHttpConfigurer::disable) + .csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .logout(AbstractHttpConfigurer::disable); + return http.build(); + } + + @Bean + @Order(2) public SecurityFilterChain configure(HttpSecurity httpSecurity) throws Exception { // @formatter:off AntPathRequestMatcher nonAccessibleBySuspendedUserPath = @@ -1134,4 +1160,18 @@ private BearerAuthFilter bearerFilter(RequestMatcher requiresAuthenticationReque exceptionResolver.resolveException(req, res, null, forbiddenWithRemoteInfo(req))); return bearerFilter; } + + private boolean isFeesOnlyAllowed(String uri) { + return uri.equals("/ping") + || uri.equals("/whoami") + || uri.equals("/health/db") + || uri.startsWith("/authentication/") + || uri.startsWith("/fees") + || uri.startsWith("/mpbs") + || uri.equals("/delay_penalty") + || uri.startsWith("/admins") + || uri.startsWith("/managers") + || uri.startsWith("/students") + || uri.startsWith("/groups"); + } } From 3445bf38829daae6c1be4af4d320ae11f690c1d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mandresy=20Ga=C3=ABtan=20RANDRIANANTOANINA?= Date: Sun, 19 Jul 2026 00:38:36 +0300 Subject: [PATCH 08/13] chore: format code --- .../endpoint/rest/security/SecurityConf.java | 66 +++++++++---------- .../school/hei/haapi/FeesOnlyFalseIT.java | 49 +++++++------- .../java/school/hei/haapi/FeesOnlyTrueIT.java | 55 ++++++++-------- 3 files changed, 83 insertions(+), 87 deletions(-) diff --git a/src/main/java/school/hei/haapi/endpoint/rest/security/SecurityConf.java b/src/main/java/school/hei/haapi/endpoint/rest/security/SecurityConf.java index 96d35a73f..4e70e925b 100644 --- a/src/main/java/school/hei/haapi/endpoint/rest/security/SecurityConf.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/security/SecurityConf.java @@ -18,6 +18,7 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; @@ -37,7 +38,6 @@ import school.hei.haapi.repository.CorRepository; import school.hei.haapi.service.CourseAssignmentService; import school.hei.haapi.service.MonitoringStudentService; -import org.springframework.beans.factory.annotation.Value; @Configuration @Slf4j @@ -72,28 +72,26 @@ public AuthenticationManager authenticationManager() { return new ProviderManager(authProvider); } - @Bean - @Order(1) - public SecurityFilterChain feesOnlyFilterChain(HttpSecurity http) throws Exception { - if (!feesOnly) { - http - .securityMatcher(request -> false) - .authorizeHttpRequests(req -> req.anyRequest().denyAll()); - return http.build(); - } - - http - .securityMatcher(request -> !isFeesOnlyAllowed(request.getRequestURI())) - .authorizeHttpRequests(req -> req.anyRequest().denyAll()) - .cors(AbstractHttpConfigurer::disable) - .csrf(AbstractHttpConfigurer::disable) - .formLogin(AbstractHttpConfigurer::disable) - .logout(AbstractHttpConfigurer::disable); - return http.build(); + @Bean + @Order(1) + public SecurityFilterChain feesOnlyFilterChain(HttpSecurity http) throws Exception { + if (!feesOnly) { + http.securityMatcher(request -> false) + .authorizeHttpRequests(req -> req.anyRequest().denyAll()); + return http.build(); } - @Bean - @Order(2) + http.securityMatcher(request -> !isFeesOnlyAllowed(request.getRequestURI())) + .authorizeHttpRequests(req -> req.anyRequest().denyAll()) + .cors(AbstractHttpConfigurer::disable) + .csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .logout(AbstractHttpConfigurer::disable); + return http.build(); + } + + @Bean + @Order(2) public SecurityFilterChain configure(HttpSecurity httpSecurity) throws Exception { // @formatter:off AntPathRequestMatcher nonAccessibleBySuspendedUserPath = @@ -1161,17 +1159,17 @@ private BearerAuthFilter bearerFilter(RequestMatcher requiresAuthenticationReque return bearerFilter; } - private boolean isFeesOnlyAllowed(String uri) { - return uri.equals("/ping") - || uri.equals("/whoami") - || uri.equals("/health/db") - || uri.startsWith("/authentication/") - || uri.startsWith("/fees") - || uri.startsWith("/mpbs") - || uri.equals("/delay_penalty") - || uri.startsWith("/admins") - || uri.startsWith("/managers") - || uri.startsWith("/students") - || uri.startsWith("/groups"); - } + private boolean isFeesOnlyAllowed(String uri) { + return uri.equals("/ping") + || uri.equals("/whoami") + || uri.equals("/health/db") + || uri.startsWith("/authentication/") + || uri.startsWith("/fees") + || uri.startsWith("/mpbs") + || uri.equals("/delay_penalty") + || uri.startsWith("/admins") + || uri.startsWith("/managers") + || uri.startsWith("/students") + || uri.startsWith("/groups"); + } } diff --git a/src/test/java/school/hei/haapi/FeesOnlyFalseIT.java b/src/test/java/school/hei/haapi/FeesOnlyFalseIT.java index a297972b7..4d40b2a44 100644 --- a/src/test/java/school/hei/haapi/FeesOnlyFalseIT.java +++ b/src/test/java/school/hei/haapi/FeesOnlyFalseIT.java @@ -21,28 +21,27 @@ @TestPropertySource(properties = "FEES_ONLY=false") class FeesOnlyDisabledIT extends FacadeITMockedThirdParties { - private ApiClient anApiClient(String token) { - return TestUtils.anApiClient(token, localPort); - } - - @BeforeEach - void setUp() { - setUpCasdoor(casdoorAuthServiceMock, certificateLoaderMock); - setUpCognito(cognitoComponentMock); - } - - @Test - void fees_only_endpoint_is_accessible_when_fees_only_disabled() { - var api = new PayingApi(anApiClient(MANAGER1_TOKEN)); - - assertDoesNotThrow( - () -> api.getFees(null, null, null, null, null, null, 1, 10, false, null)); - } - - @Test - void non_fees_only_endpoint_is_accessible_when_fees_only_disabled() { - var api = new UsersApi(anApiClient(MANAGER1_TOKEN)); - - assertDoesNotThrow(() -> api.getMonitors(1, 10, null, null, null)); - } -} \ No newline at end of file + private ApiClient anApiClient(String token) { + return TestUtils.anApiClient(token, localPort); + } + + @BeforeEach + void setUp() { + setUpCasdoor(casdoorAuthServiceMock, certificateLoaderMock); + setUpCognito(cognitoComponentMock); + } + + @Test + void fees_only_endpoint_is_accessible_when_fees_only_disabled() { + var api = new PayingApi(anApiClient(MANAGER1_TOKEN)); + + assertDoesNotThrow(() -> api.getFees(null, null, null, null, null, null, 1, 10, false, null)); + } + + @Test + void non_fees_only_endpoint_is_accessible_when_fees_only_disabled() { + var api = new UsersApi(anApiClient(MANAGER1_TOKEN)); + + assertDoesNotThrow(() -> api.getMonitors(1, 10, null, null, null)); + } +} diff --git a/src/test/java/school/hei/haapi/FeesOnlyTrueIT.java b/src/test/java/school/hei/haapi/FeesOnlyTrueIT.java index cf242a492..330e2ee59 100644 --- a/src/test/java/school/hei/haapi/FeesOnlyTrueIT.java +++ b/src/test/java/school/hei/haapi/FeesOnlyTrueIT.java @@ -24,31 +24,30 @@ @TestPropertySource(properties = "FEES_ONLY=true") class FeesOnlyEnabledIT extends FacadeITMockedThirdParties { - private ApiClient anApiClient(String token) { - return TestUtils.anApiClient(token, localPort); - } - - @BeforeEach - void setUp() { - setUpCasdoor(casdoorAuthServiceMock, certificateLoaderMock); - setUpCognito(cognitoComponentMock); - } - - @Test - void fees_only_endpoint_stays_accessible_when_fees_only_enabled() { - var api = new PayingApi(anApiClient(MANAGER1_TOKEN)); - - assertDoesNotThrow( - () -> api.getFees(null, null, null, null, null, null, 1, 10, false, null)); - } - - @Test - void non_fees_only_endpoint_is_blocked_when_fees_only_enabled() { - var api = new UsersApi(anApiClient(MANAGER1_TOKEN)); - - var exception = - assertThrows(ApiException.class, () -> api.getMonitors(1, 10, null, null, null)); - - assertEquals(403, exception.getCode()); - } -} \ No newline at end of file + private ApiClient anApiClient(String token) { + return TestUtils.anApiClient(token, localPort); + } + + @BeforeEach + void setUp() { + setUpCasdoor(casdoorAuthServiceMock, certificateLoaderMock); + setUpCognito(cognitoComponentMock); + } + + @Test + void fees_only_endpoint_stays_accessible_when_fees_only_enabled() { + var api = new PayingApi(anApiClient(MANAGER1_TOKEN)); + + assertDoesNotThrow(() -> api.getFees(null, null, null, null, null, null, 1, 10, false, null)); + } + + @Test + void non_fees_only_endpoint_is_blocked_when_fees_only_enabled() { + var api = new UsersApi(anApiClient(MANAGER1_TOKEN)); + + var exception = + assertThrows(ApiException.class, () -> api.getMonitors(1, 10, null, null, null)); + + assertEquals(403, exception.getCode()); + } +} From 0712da4f036e8e05adadede952afe30547c298bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mandresy=20Ga=C3=ABtan=20RANDRIANANTOANINA?= Date: Sun, 19 Jul 2026 02:47:00 +0300 Subject: [PATCH 09/13] refactor: restrict student endpoints in feesOnly mode --- .../hei/haapi/endpoint/rest/security/SecurityConf.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/school/hei/haapi/endpoint/rest/security/SecurityConf.java b/src/main/java/school/hei/haapi/endpoint/rest/security/SecurityConf.java index 4e70e925b..1936bf6b8 100644 --- a/src/main/java/school/hei/haapi/endpoint/rest/security/SecurityConf.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/security/SecurityConf.java @@ -1169,7 +1169,8 @@ private boolean isFeesOnlyAllowed(String uri) { || uri.equals("/delay_penalty") || uri.startsWith("/admins") || uri.startsWith("/managers") - || uri.startsWith("/students") - || uri.startsWith("/groups"); + || uri.equals("/students") + || uri.matches("/students/[^/]+$") + || uri.matches("/students/[^/]+/fees.*"); } } From 92b09376eb4043bb5e4d8640b0486c7d8e5cff54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mandresy=20Ga=C3=ABtan=20RANDRIANANTOANINA?= Date: Tue, 21 Jul 2026 08:57:07 +0300 Subject: [PATCH 10/13] refactor(security): extract feesOnly logic into dedicated classes --- .../rest/security/FeesOnlySecurityConfig.java | 40 ++++++++++++++++ .../rest/security/FeesOnlyUriMatcher.java | 47 +++++++++++++++++++ .../endpoint/rest/security/SecurityConf.java | 37 --------------- 3 files changed, 87 insertions(+), 37 deletions(-) create mode 100644 src/main/java/school/hei/haapi/endpoint/rest/security/FeesOnlySecurityConfig.java create mode 100644 src/main/java/school/hei/haapi/endpoint/rest/security/FeesOnlyUriMatcher.java diff --git a/src/main/java/school/hei/haapi/endpoint/rest/security/FeesOnlySecurityConfig.java b/src/main/java/school/hei/haapi/endpoint/rest/security/FeesOnlySecurityConfig.java new file mode 100644 index 000000000..707bfea4c --- /dev/null +++ b/src/main/java/school/hei/haapi/endpoint/rest/security/FeesOnlySecurityConfig.java @@ -0,0 +1,40 @@ +package school.hei.haapi.endpoint.rest.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.beans.factory.annotation.Value; + +@Configuration +public class FeesOnlySecurityConfig { + + private final FeesOnlyUriMatcher feesOnlyUriMatcher; + + @Value("${FEES_ONLY:false}") + private boolean feesOnly; + + public FeesOnlySecurityConfig(FeesOnlyUriMatcher feesOnlyUriMatcher) { + this.feesOnlyUriMatcher = feesOnlyUriMatcher; + } + + @Bean + @Order(1) + public SecurityFilterChain feesOnlyFilterChain(HttpSecurity http) throws Exception { + if (!feesOnly) { + http.securityMatcher(request -> false) + .authorizeHttpRequests(req -> req.anyRequest().denyAll()); + return http.build(); + } + + http.securityMatcher(request -> !feesOnlyUriMatcher.isAllowed(request.getRequestURI())) + .authorizeHttpRequests(req -> req.anyRequest().denyAll()) + .cors(AbstractHttpConfigurer::disable) + .csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .logout(AbstractHttpConfigurer::disable); + return http.build(); + } +} diff --git a/src/main/java/school/hei/haapi/endpoint/rest/security/FeesOnlyUriMatcher.java b/src/main/java/school/hei/haapi/endpoint/rest/security/FeesOnlyUriMatcher.java new file mode 100644 index 000000000..3329ed3ad --- /dev/null +++ b/src/main/java/school/hei/haapi/endpoint/rest/security/FeesOnlyUriMatcher.java @@ -0,0 +1,47 @@ +package school.hei.haapi.endpoint.rest.security; + +import java.util.Arrays; +import java.util.Set; +import java.util.regex.Pattern; +import org.springframework.stereotype.Component; + +@Component +public class FeesOnlyUriMatcher { + + private static final Set ALLOWED_STUDENT_ROUTE_SEGMENTS = Set.of( + "stats", + "level" + ); + + private static final Pattern STUDENT_ID = Pattern.compile("^[0-9a-fA-F-]{8,}$"); + + public boolean isAllowed(String uri) { + return uri.equals("/ping") + || uri.equals("/whoami") + || uri.equals("/health/db") + || uri.startsWith("/authentication/") + || uri.startsWith("/fees") + || uri.startsWith("/mpbs") + || uri.equals("/delay_penalty") + || uri.startsWith("/admins") + || uri.startsWith("/managers") + || uri.equals("/students") + || isStudentByIdOrAllowedSegment(uri) + || isStudentFees(uri); + } + + private boolean isStudentByIdOrAllowedSegment(String uri) { + if (!uri.startsWith("/students/")) { + return false; + } + String segment = uri.substring("/students/".length()); + if (STUDENT_ID.matcher(segment).matches()) { + return true; + } + return Arrays.stream(uri.split("/")).anyMatch(ALLOWED_STUDENT_ROUTE_SEGMENTS::contains); + } + + private boolean isStudentFees(String uri) { + return uri.startsWith("/students/") && uri.contains("/fees"); + } +} \ No newline at end of file diff --git a/src/main/java/school/hei/haapi/endpoint/rest/security/SecurityConf.java b/src/main/java/school/hei/haapi/endpoint/rest/security/SecurityConf.java index 1936bf6b8..42f9698d6 100644 --- a/src/main/java/school/hei/haapi/endpoint/rest/security/SecurityConf.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/security/SecurityConf.java @@ -18,7 +18,6 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; @@ -50,9 +49,6 @@ public class SecurityConf { private final HandlerExceptionResolver exceptionResolver; private final CorRepository corRepository; - @Value("${FEES_ONLY:false}") - private boolean feesOnly; - public SecurityConf( CasdoorAuthProvider authProvider, // InternalToExternalErrorHandler behind @@ -72,24 +68,6 @@ public AuthenticationManager authenticationManager() { return new ProviderManager(authProvider); } - @Bean - @Order(1) - public SecurityFilterChain feesOnlyFilterChain(HttpSecurity http) throws Exception { - if (!feesOnly) { - http.securityMatcher(request -> false) - .authorizeHttpRequests(req -> req.anyRequest().denyAll()); - return http.build(); - } - - http.securityMatcher(request -> !isFeesOnlyAllowed(request.getRequestURI())) - .authorizeHttpRequests(req -> req.anyRequest().denyAll()) - .cors(AbstractHttpConfigurer::disable) - .csrf(AbstractHttpConfigurer::disable) - .formLogin(AbstractHttpConfigurer::disable) - .logout(AbstractHttpConfigurer::disable); - return http.build(); - } - @Bean @Order(2) public SecurityFilterChain configure(HttpSecurity httpSecurity) throws Exception { @@ -1158,19 +1136,4 @@ private BearerAuthFilter bearerFilter(RequestMatcher requiresAuthenticationReque exceptionResolver.resolveException(req, res, null, forbiddenWithRemoteInfo(req))); return bearerFilter; } - - private boolean isFeesOnlyAllowed(String uri) { - return uri.equals("/ping") - || uri.equals("/whoami") - || uri.equals("/health/db") - || uri.startsWith("/authentication/") - || uri.startsWith("/fees") - || uri.startsWith("/mpbs") - || uri.equals("/delay_penalty") - || uri.startsWith("/admins") - || uri.startsWith("/managers") - || uri.equals("/students") - || uri.matches("/students/[^/]+$") - || uri.matches("/students/[^/]+/fees.*"); - } } From 8744e85f8fbc152e1705be549f72fa90b38ab71e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mandresy=20Ga=C3=ABtan=20RANDRIANANTOANINA?= Date: Tue, 21 Jul 2026 09:00:34 +0300 Subject: [PATCH 11/13] format code --- .../rest/security/FeesOnlySecurityConfig.java | 46 +++++++------- .../rest/security/FeesOnlyUriMatcher.java | 61 +++++++++---------- 2 files changed, 52 insertions(+), 55 deletions(-) diff --git a/src/main/java/school/hei/haapi/endpoint/rest/security/FeesOnlySecurityConfig.java b/src/main/java/school/hei/haapi/endpoint/rest/security/FeesOnlySecurityConfig.java index 707bfea4c..43a3fbfbb 100644 --- a/src/main/java/school/hei/haapi/endpoint/rest/security/FeesOnlySecurityConfig.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/security/FeesOnlySecurityConfig.java @@ -1,40 +1,40 @@ package school.hei.haapi.endpoint.rest.security; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.beans.factory.annotation.Value; @Configuration public class FeesOnlySecurityConfig { - private final FeesOnlyUriMatcher feesOnlyUriMatcher; + private final FeesOnlyUriMatcher feesOnlyUriMatcher; - @Value("${FEES_ONLY:false}") - private boolean feesOnly; + @Value("${FEES_ONLY:false}") + private boolean feesOnly; - public FeesOnlySecurityConfig(FeesOnlyUriMatcher feesOnlyUriMatcher) { - this.feesOnlyUriMatcher = feesOnlyUriMatcher; - } + public FeesOnlySecurityConfig(FeesOnlyUriMatcher feesOnlyUriMatcher) { + this.feesOnlyUriMatcher = feesOnlyUriMatcher; + } - @Bean - @Order(1) - public SecurityFilterChain feesOnlyFilterChain(HttpSecurity http) throws Exception { - if (!feesOnly) { - http.securityMatcher(request -> false) - .authorizeHttpRequests(req -> req.anyRequest().denyAll()); - return http.build(); - } - - http.securityMatcher(request -> !feesOnlyUriMatcher.isAllowed(request.getRequestURI())) - .authorizeHttpRequests(req -> req.anyRequest().denyAll()) - .cors(AbstractHttpConfigurer::disable) - .csrf(AbstractHttpConfigurer::disable) - .formLogin(AbstractHttpConfigurer::disable) - .logout(AbstractHttpConfigurer::disable); - return http.build(); + @Bean + @Order(1) + public SecurityFilterChain feesOnlyFilterChain(HttpSecurity http) throws Exception { + if (!feesOnly) { + http.securityMatcher(request -> false) + .authorizeHttpRequests(req -> req.anyRequest().denyAll()); + return http.build(); } + + http.securityMatcher(request -> !feesOnlyUriMatcher.isAllowed(request.getRequestURI())) + .authorizeHttpRequests(req -> req.anyRequest().denyAll()) + .cors(AbstractHttpConfigurer::disable) + .csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .logout(AbstractHttpConfigurer::disable); + return http.build(); + } } diff --git a/src/main/java/school/hei/haapi/endpoint/rest/security/FeesOnlyUriMatcher.java b/src/main/java/school/hei/haapi/endpoint/rest/security/FeesOnlyUriMatcher.java index 3329ed3ad..644e6d3aa 100644 --- a/src/main/java/school/hei/haapi/endpoint/rest/security/FeesOnlyUriMatcher.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/security/FeesOnlyUriMatcher.java @@ -8,40 +8,37 @@ @Component public class FeesOnlyUriMatcher { - private static final Set ALLOWED_STUDENT_ROUTE_SEGMENTS = Set.of( - "stats", - "level" - ); + private static final Set ALLOWED_STUDENT_ROUTE_SEGMENTS = Set.of("stats", "level"); - private static final Pattern STUDENT_ID = Pattern.compile("^[0-9a-fA-F-]{8,}$"); + private static final Pattern STUDENT_ID = Pattern.compile("^[0-9a-fA-F-]{8,}$"); - public boolean isAllowed(String uri) { - return uri.equals("/ping") - || uri.equals("/whoami") - || uri.equals("/health/db") - || uri.startsWith("/authentication/") - || uri.startsWith("/fees") - || uri.startsWith("/mpbs") - || uri.equals("/delay_penalty") - || uri.startsWith("/admins") - || uri.startsWith("/managers") - || uri.equals("/students") - || isStudentByIdOrAllowedSegment(uri) - || isStudentFees(uri); - } + public boolean isAllowed(String uri) { + return uri.equals("/ping") + || uri.equals("/whoami") + || uri.equals("/health/db") + || uri.startsWith("/authentication/") + || uri.startsWith("/fees") + || uri.startsWith("/mpbs") + || uri.equals("/delay_penalty") + || uri.startsWith("/admins") + || uri.startsWith("/managers") + || uri.equals("/students") + || isStudentByIdOrAllowedSegment(uri) + || isStudentFees(uri); + } - private boolean isStudentByIdOrAllowedSegment(String uri) { - if (!uri.startsWith("/students/")) { - return false; - } - String segment = uri.substring("/students/".length()); - if (STUDENT_ID.matcher(segment).matches()) { - return true; - } - return Arrays.stream(uri.split("/")).anyMatch(ALLOWED_STUDENT_ROUTE_SEGMENTS::contains); + private boolean isStudentByIdOrAllowedSegment(String uri) { + if (!uri.startsWith("/students/")) { + return false; } - - private boolean isStudentFees(String uri) { - return uri.startsWith("/students/") && uri.contains("/fees"); + String segment = uri.substring("/students/".length()); + if (STUDENT_ID.matcher(segment).matches()) { + return true; } -} \ No newline at end of file + return Arrays.stream(uri.split("/")).anyMatch(ALLOWED_STUDENT_ROUTE_SEGMENTS::contains); + } + + private boolean isStudentFees(String uri) { + return uri.startsWith("/students/") && uri.contains("/fees"); + } +} From 1557af04037d7e3311c16322ad6903bb91ae9759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mandresy=20Ga=C3=ABtan=20RANDRIANANTOANINA?= Date: Wed, 29 Jul 2026 01:06:13 +0300 Subject: [PATCH 12/13] chore: relancing verification From ccebc6a7328edde204f7f71da053c636bc71a975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mandresy=20Ga=C3=ABtan=20RANDRIANANTOANINA?= Date: Wed, 29 Jul 2026 09:13:55 +0300 Subject: [PATCH 13/13] fix(security): allow feeTemplates, feeCreationJobs and delay_penalty_change in FeesOnly mode --- .../rest/security/FeesOnlyUriMatcher.java | 61 ++++++++++--------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/src/main/java/school/hei/haapi/endpoint/rest/security/FeesOnlyUriMatcher.java b/src/main/java/school/hei/haapi/endpoint/rest/security/FeesOnlyUriMatcher.java index 644e6d3aa..ae88fa941 100644 --- a/src/main/java/school/hei/haapi/endpoint/rest/security/FeesOnlyUriMatcher.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/security/FeesOnlyUriMatcher.java @@ -8,37 +8,38 @@ @Component public class FeesOnlyUriMatcher { - private static final Set ALLOWED_STUDENT_ROUTE_SEGMENTS = Set.of("stats", "level"); + private static final Set ALLOWED_STUDENT_ROUTE_SEGMENTS = Set.of("stats", "level"); + private static final Pattern STUDENT_ID = Pattern.compile("^[0-9a-fA-F-]{8,}$"); - private static final Pattern STUDENT_ID = Pattern.compile("^[0-9a-fA-F-]{8,}$"); - - public boolean isAllowed(String uri) { - return uri.equals("/ping") - || uri.equals("/whoami") - || uri.equals("/health/db") - || uri.startsWith("/authentication/") - || uri.startsWith("/fees") - || uri.startsWith("/mpbs") - || uri.equals("/delay_penalty") - || uri.startsWith("/admins") - || uri.startsWith("/managers") - || uri.equals("/students") - || isStudentByIdOrAllowedSegment(uri) - || isStudentFees(uri); - } - - private boolean isStudentByIdOrAllowedSegment(String uri) { - if (!uri.startsWith("/students/")) { - return false; + public boolean isAllowed(String uri) { + return uri.equals("/ping") + || uri.equals("/whoami") + || uri.equals("/health/db") + || uri.startsWith("/authentication/") + || uri.startsWith("/fees") + || uri.startsWith("/feeTemplates") + || uri.startsWith("/feeCreationJobs") + || uri.startsWith("/mpbs") + || uri.startsWith("/delay_penalty") + || uri.startsWith("/admins") + || uri.startsWith("/managers") + || uri.equals("/students") + || isStudentByIdOrAllowedSegment(uri) + || isStudentFees(uri); } - String segment = uri.substring("/students/".length()); - if (STUDENT_ID.matcher(segment).matches()) { - return true; + + private boolean isStudentByIdOrAllowedSegment(String uri) { + if (!uri.startsWith("/students/")) { + return false; + } + String segment = uri.substring("/students/".length()); + if (STUDENT_ID.matcher(segment).matches()) { + return true; + } + return Arrays.stream(uri.split("/")).anyMatch(ALLOWED_STUDENT_ROUTE_SEGMENTS::contains); } - return Arrays.stream(uri.split("/")).anyMatch(ALLOWED_STUDENT_ROUTE_SEGMENTS::contains); - } - private boolean isStudentFees(String uri) { - return uri.startsWith("/students/") && uri.contains("/fees"); - } -} + private boolean isStudentFees(String uri) { + return uri.startsWith("/students/") && uri.contains("/fees"); + } +} \ No newline at end of file