Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +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;

@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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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<String> 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("/feeTemplates")
|| uri.startsWith("/feeCreationJobs")
|| uri.startsWith("/mpbs")
|| uri.startsWith("/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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -68,6 +69,7 @@ public AuthenticationManager authenticationManager() {
}

@Bean
@Order(2)
public SecurityFilterChain configure(HttpSecurity httpSecurity) throws Exception {
// @formatter:off
AntPathRequestMatcher nonAccessibleBySuspendedUserPath =
Expand Down
47 changes: 47 additions & 0 deletions src/test/java/school/hei/haapi/FeesOnlyFalseIT.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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;

@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));
}
}
53 changes: 53 additions & 0 deletions src/test/java/school/hei/haapi/FeesOnlyTrueIT.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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;

@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));

var exception =
assertThrows(ApiException.class, () -> api.getMonitors(1, 10, null, null, null));

assertEquals(403, exception.getCode());
}
}