diff --git a/.gitignore b/.gitignore index 8c901fb7..b2e7b18b 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,6 @@ out/ **.toml ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store + +.env \ No newline at end of file diff --git a/build.gradle b/build.gradle index ba7cc269..41f7b3d0 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ plugins { id 'jacoco' - + } jacoco { @@ -154,5 +154,7 @@ implementation 'org.postgresql:postgresql' implementation("org.apache.poi:poi:5.2.5") implementation("org.apache.poi:poi-ooxml:5.2.5") +implementation 'org.springframework.boot:spring-boot-starter-security' +implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' } diff --git a/src/main/java/school/hei/vola/endpoint/rest/security/SecurityConf.java b/src/main/java/school/hei/vola/endpoint/rest/security/SecurityConf.java new file mode 100644 index 00000000..2b28f688 --- /dev/null +++ b/src/main/java/school/hei/vola/endpoint/rest/security/SecurityConf.java @@ -0,0 +1,57 @@ +package school.hei.vola.endpoint.rest.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConf { + + private final VolaAdminChecker volaAdminChecker; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.authorizeHttpRequests( + auth -> + auth.requestMatchers( + "/ping", + "/health/**", + "/error", + "/swagger-ui/**", + "/v3/api-docs/**", + "/payment", + "/payments/search", + "/orange/**") + .permitAll() + .requestMatchers("/payments/**") + .authenticated() + .anyRequest() + .denyAll()) + .oauth2Login(oauth2 -> oauth2.successHandler(authenticationSuccessHandler())) + .logout(LogoutConfigurer::permitAll) + .csrf(csrf -> csrf.ignoringRequestMatchers("/payment", "/payments/search", "/orange/**")); + + return http.build(); + } + + @Bean + public AuthenticationSuccessHandler authenticationSuccessHandler() { + return (request, response, authentication) -> { + var oidcUser = (OidcUser) authentication.getPrincipal(); + String email = oidcUser.getEmail(); + if (!volaAdminChecker.isAdmin(email)) { + throw new AccessDeniedException("You're not a Vola Administrator"); + } + response.sendRedirect("/payments"); + }; + } +} diff --git a/src/main/java/school/hei/vola/endpoint/rest/security/VolaAdminChecker.java b/src/main/java/school/hei/vola/endpoint/rest/security/VolaAdminChecker.java new file mode 100644 index 00000000..d8a958ec --- /dev/null +++ b/src/main/java/school/hei/vola/endpoint/rest/security/VolaAdminChecker.java @@ -0,0 +1,24 @@ +package school.hei.vola.endpoint.rest.security; + +import java.util.Arrays; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class VolaAdminChecker { + private String admins; + + public VolaAdminChecker(@Value("${vola.admins}") String admins) { + this.admins = admins; + } + + public boolean isAdmin(String email) { + if (email == null || admins == null) { + return false; + } + return Arrays.stream(admins.split(",")) + .map(String::trim) + .map(String::toLowerCase) + .anyMatch(admin -> admin.equals(email.toLowerCase())); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 00000000..fab909e4 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,16 @@ +spring: + security: + oauth2: + client: + registration: + casdoor: + client-id: ${CASDOOR_CLIENT_ID} + client-secret: ${CASDOOR_CLIENT_SECRET} + scope: openid, profile, email + authorization-grant-type: authorization_code + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" + provider: + casdoor: + issuer-uri: ${CASDOOR_ISSUER_URI} +vola: + admins: ${VOLA_ADMINS} diff --git a/src/test/java/school/hei/vola/conf/EnvConf.java b/src/test/java/school/hei/vola/conf/EnvConf.java index b27597cb..313a96d5 100644 --- a/src/test/java/school/hei/vola/conf/EnvConf.java +++ b/src/test/java/school/hei/vola/conf/EnvConf.java @@ -10,6 +10,20 @@ void configureProperties(DynamicPropertyRegistry registry) { "spring.datasource.url", () -> "jdbc:h2:mem:testdb;CASE_INSENSITIVE_IDENTIFIERS=TRUE"); registry.add("spring.datasource.driverClassName", () -> "org.h2.Driver"); registry.add("spring.jpa.database-platform", () -> "org.hibernate.dialect.H2Dialect"); - registry.add("orange.api.url", () -> apiUrl); + registry.add("orange.api.url", () -> apiUrl != null ? apiUrl : "http://dummy.orange.api"); + registry.add( + "spring.security.oauth2.client.registration.casdoor.provider", () -> "casdoor-test"); + registry.add("spring.security.oauth2.client.registration.casdoor.client-id", () -> "dummy"); + registry.add("spring.security.oauth2.client.registration.casdoor.client-secret", () -> "dummy"); + registry.add( + "spring.security.oauth2.client.registration.casdoor.authorization-grant-type", + () -> "authorization_code"); + registry.add( + "spring.security.oauth2.client.registration.casdoor.redirect-uri", + () -> "{baseUrl}/login/oauth2/code/casdoor"); + registry.add( + "spring.security.oauth2.client.provider.casdoor-test.authorization-uri", () -> "dummy"); + registry.add("spring.security.oauth2.client.provider.casdoor-test.token-uri", () -> "dummy"); + registry.add("vola.admins", () -> "admin@test.com"); } } diff --git a/src/test/java/school/hei/vola/unit/VolaAdminCheckerTest.java b/src/test/java/school/hei/vola/unit/VolaAdminCheckerTest.java new file mode 100644 index 00000000..18af1201 --- /dev/null +++ b/src/test/java/school/hei/vola/unit/VolaAdminCheckerTest.java @@ -0,0 +1,36 @@ +package school.hei.vola.unit; + +import org.junit.jupiter.api.Test; +import school.hei.vola.endpoint.rest.security.VolaAdminChecker; + +class VolaAdminCheckerTest { + @Test + void admin_email_returns_true() { + var checker = new VolaAdminChecker("admin@hei.school, bob@hei.school"); + assert checker.isAdmin("admin@hei.school"); + } + + @Test + void admin_email_case_insensitive() { + var checker = new VolaAdminChecker("admin@hei.school,bob@hei.school"); + assert checker.isAdmin("bob@Hei.School"); + } + + @Test + void non_admin_email_returns_false() { + var checker = new VolaAdminChecker("admin@hei.school,bob@hei.school"); + assert !checker.isAdmin("other@hei.school"); + } + + @Test + void null_email_returns_false() { + var checker = new VolaAdminChecker("admin@hei.school, valisoa@hei.school"); + assert !checker.isAdmin(null); + } + + @Test + void second_admin_also_matches() { + var checker = new VolaAdminChecker("admin@hei.school,valisoa@hei.school"); + assert checker.isAdmin("valisoa@hei.school"); + } +}