Loading movie-microservice/src/main/java/cz/muni/fi/iamdb/MovieMicroservice.java +0 −122 Original line number Diff line number Diff line package cz.muni.fi.iamdb; import io.swagger.v3.oas.models.security.OAuthFlow; import io.swagger.v3.oas.models.security.OAuthFlows; import io.swagger.v3.oas.models.security.Scopes; import io.swagger.v3.oas.models.security.SecurityScheme; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springdoc.core.customizers.OpenApiCustomizer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent; import org.springframework.context.annotation.Bean; import org.springframework.context.event.EventListener; import org.springframework.http.HttpMethod; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; @SpringBootApplication public class MovieMicroservice { private static final Logger log = LoggerFactory.getLogger(MovieMicroservice.class); private static final String SECURITY_SCHEME_OAUTH2 = "MUNI"; private static final String SECURITY_SCHEME_BEARER = "Bearer"; public static void main(String[] args) { SpringApplication.run(MovieMicroservice.class, args); } /** * Configure access restrictions to the API. * Introspection of opaque access token is configured, introspection endpoint is defined in application.yml. */ @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { // todo extract from properties final String MOVIE_POST_SCOPE = "SCOPE_test_1"; final String MOVIE_GET_ALL_SCOPE = "SCOPE_test_2"; final String MOVIE_GET_SPECIFIC_SCOPE = "SCOPE_test_3"; final String MOVIE_PUT_SCOPE = "SCOPE_test_4"; final String MOVIE_DELETE_SCOPE = "SCOPE_test_5"; final String SPECIFIC_MOVIES_IMAGES_GET_SCOPE = "SCOPE_test_5"; // out of scopes :( final String IMAGES_POST_SCOPE = "SCOPE_test_1"; final String IMAGES_GET_ALL_SCOPE = "SCOPE_test_2"; final String IMAGES_GET_SPECIFIC_SCOPE = "SCOPE_test_3"; final String IMAGES_PUT_SCOPE = "SCOPE_test_4"; final String IMAGES_DELETE_SCOPE = "SCOPE_test_5"; final String GENRES_POST_SCOPE = "SCOPE_test_1"; final String GENRES_GET_ALL_SCOPE = "SCOPE_test_2"; final String GENRES_GET_SPECIFIC_SCOPE = "SCOPE_test_3"; final String GENRES_PUT_SCOPE = "SCOPE_test_4"; final String GENRES_DELETE_SCOPE = "SCOPE_test_5"; http .authorizeHttpRequests(x -> x .requestMatchers(HttpMethod.POST, "/").hasAuthority(MOVIE_POST_SCOPE) .requestMatchers(HttpMethod.GET, "/").hasAuthority(MOVIE_GET_ALL_SCOPE) .requestMatchers(HttpMethod.GET, "/{id}").hasAuthority(MOVIE_GET_SPECIFIC_SCOPE) .requestMatchers(HttpMethod.PUT, "/{id}").hasAuthority(MOVIE_PUT_SCOPE) .requestMatchers(HttpMethod.DELETE, "/{id}").hasAuthority(MOVIE_DELETE_SCOPE) .requestMatchers(HttpMethod.GET, "/{id}/images").hasAuthority(SPECIFIC_MOVIES_IMAGES_GET_SCOPE) // images .requestMatchers(HttpMethod.POST, "/images").hasAuthority(IMAGES_POST_SCOPE) .requestMatchers(HttpMethod.GET, "/images").hasAuthority(IMAGES_GET_ALL_SCOPE) .requestMatchers(HttpMethod.GET, "/images/{id}").hasAuthority(IMAGES_GET_SPECIFIC_SCOPE) .requestMatchers(HttpMethod.PUT, "/images/{id}").hasAuthority(IMAGES_PUT_SCOPE) .requestMatchers(HttpMethod.DELETE, "/images/{id}").hasAuthority(IMAGES_DELETE_SCOPE) // genres .requestMatchers(HttpMethod.POST, "/genres").hasAuthority(GENRES_POST_SCOPE) .requestMatchers(HttpMethod.GET, "/genres").hasAuthority(GENRES_GET_ALL_SCOPE) .requestMatchers(HttpMethod.GET, "/genres/{id}").hasAuthority(GENRES_GET_SPECIFIC_SCOPE) .requestMatchers(HttpMethod.PUT, "/genres/{id}").hasAuthority(GENRES_PUT_SCOPE) .requestMatchers(HttpMethod.DELETE, "/genres/{id}").hasAuthority(GENRES_DELETE_SCOPE) // defensively deny all other requests // todo allow the seeding endpoints (or refactor the seeder) .anyRequest().denyAll() ) .oauth2ResourceServer(oauth2 -> oauth2.opaqueToken(Customizer.withDefaults())) ; return http.build(); } /** * Add security definitions to generated openapi.yaml. */ @Bean public OpenApiCustomizer openAPICustomizer() { return openApi -> { log.info("adding security to OpenAPI description"); openApi.getComponents() .addSecuritySchemes(SECURITY_SCHEME_OAUTH2, new SecurityScheme() .type(SecurityScheme.Type.OAUTH2) .description("get access token with OAuth 2 Authorization Code Grant") .flows(new OAuthFlows() .authorizationCode(new OAuthFlow() .authorizationUrl("https://oidc.muni.cz/oidc/authorize") .tokenUrl("https://oidc.muni.cz/oidc/token") .scopes(new Scopes() .addString("test_read", "reading things") .addString("test_write", "creating things") .addString("test_1", "deleting things") ) ) ) ) .addSecuritySchemes(SECURITY_SCHEME_BEARER, new SecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme("bearer") .description("provide a valid access token") ) ; }; } /** * Display a hint in the log. */ @EventListener public void onApplicationEvent(ServletWebServerInitializedEvent event) { log.info("**************************"); int port = event.getWebServer().getPort(); log.info("visit http://localhost:{}/swagger-ui.html for UI", port); log.info("visit http://localhost:{}/openapi.yaml for OpenAPI document", port); log.info("**************************"); } } movie-microservice/src/main/java/cz/muni/fi/iamdb/config/SecurityConfig.java 0 → 100644 +129 −0 Original line number Diff line number Diff line package cz.muni.fi.iamdb.config; import cz.muni.fi.iamdb.MovieMicroservice; import io.swagger.v3.oas.models.security.OAuthFlow; import io.swagger.v3.oas.models.security.OAuthFlows; import io.swagger.v3.oas.models.security.Scopes; import io.swagger.v3.oas.models.security.SecurityScheme; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springdoc.core.customizers.OpenApiCustomizer; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent; import org.springframework.context.annotation.Bean; import org.springframework.context.event.EventListener; import org.springframework.http.HttpMethod; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; @SpringBootApplication public class SecurityConfig { private static final Logger log = LoggerFactory.getLogger(MovieMicroservice.class); private static final String SECURITY_SCHEME_OAUTH2 = "MUNI"; private static final String SECURITY_SCHEME_BEARER = "Bearer"; /** * Configure access restrictions to the API. * Introspection of opaque access token is configured, introspection endpoint is defined in application.yml. */ @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { // todo extract from properties final String MOVIE_POST_SCOPE = "SCOPE_test_1"; final String MOVIE_GET_ALL_SCOPE = "SCOPE_test_2"; final String MOVIE_GET_SPECIFIC_SCOPE = "SCOPE_test_3"; final String MOVIE_PUT_SCOPE = "SCOPE_test_4"; final String MOVIE_DELETE_SCOPE = "SCOPE_test_5"; final String SPECIFIC_MOVIES_IMAGES_GET_SCOPE = "SCOPE_test_5"; // out of scopes :( final String IMAGES_POST_SCOPE = "SCOPE_test_1"; final String IMAGES_GET_ALL_SCOPE = "SCOPE_test_2"; final String IMAGES_GET_SPECIFIC_SCOPE = "SCOPE_test_3"; final String IMAGES_PUT_SCOPE = "SCOPE_test_4"; final String IMAGES_DELETE_SCOPE = "SCOPE_test_5"; final String GENRES_POST_SCOPE = "SCOPE_test_1"; final String GENRES_GET_ALL_SCOPE = "SCOPE_test_2"; final String GENRES_GET_SPECIFIC_SCOPE = "SCOPE_test_3"; final String GENRES_PUT_SCOPE = "SCOPE_test_4"; final String GENRES_DELETE_SCOPE = "SCOPE_test_5"; http .authorizeHttpRequests(x -> x .requestMatchers(HttpMethod.POST, "/").hasAuthority(MOVIE_POST_SCOPE) .requestMatchers(HttpMethod.GET, "/").hasAuthority(MOVIE_GET_ALL_SCOPE) .requestMatchers(HttpMethod.GET, "/{id}").hasAuthority(MOVIE_GET_SPECIFIC_SCOPE) .requestMatchers(HttpMethod.PUT, "/{id}").hasAuthority(MOVIE_PUT_SCOPE) .requestMatchers(HttpMethod.DELETE, "/{id}").hasAuthority(MOVIE_DELETE_SCOPE) .requestMatchers(HttpMethod.GET, "/{id}/images").hasAuthority(SPECIFIC_MOVIES_IMAGES_GET_SCOPE) // images .requestMatchers(HttpMethod.POST, "/images").hasAuthority(IMAGES_POST_SCOPE) .requestMatchers(HttpMethod.GET, "/images").hasAuthority(IMAGES_GET_ALL_SCOPE) .requestMatchers(HttpMethod.GET, "/images/{id}").hasAuthority(IMAGES_GET_SPECIFIC_SCOPE) .requestMatchers(HttpMethod.PUT, "/images/{id}").hasAuthority(IMAGES_PUT_SCOPE) .requestMatchers(HttpMethod.DELETE, "/images/{id}").hasAuthority(IMAGES_DELETE_SCOPE) // genres .requestMatchers(HttpMethod.POST, "/genres").hasAuthority(GENRES_POST_SCOPE) .requestMatchers(HttpMethod.GET, "/genres").hasAuthority(GENRES_GET_ALL_SCOPE) .requestMatchers(HttpMethod.GET, "/genres/{id}").hasAuthority(GENRES_GET_SPECIFIC_SCOPE) .requestMatchers(HttpMethod.PUT, "/genres/{id}").hasAuthority(GENRES_PUT_SCOPE) .requestMatchers(HttpMethod.DELETE, "/genres/{id}").hasAuthority(GENRES_DELETE_SCOPE) // defensively deny all other requests // todo allow the seeding endpoints (or refactor the seeder) .anyRequest().denyAll() ) .oauth2ResourceServer(oauth2 -> oauth2.opaqueToken(Customizer.withDefaults())) ; return http.build(); } /** * Add security definitions to generated openapi.yaml. */ @Bean public OpenApiCustomizer openAPICustomizer() { return openApi -> { log.info("adding security to OpenAPI description"); openApi.getComponents() .addSecuritySchemes(SECURITY_SCHEME_OAUTH2, new SecurityScheme() .type(SecurityScheme.Type.OAUTH2) .description("get access token with OAuth 2 Authorization Code Grant") .flows(new OAuthFlows() .authorizationCode(new OAuthFlow() .authorizationUrl("https://oidc.muni.cz/oidc/authorize") .tokenUrl("https://oidc.muni.cz/oidc/token") .scopes(new Scopes() .addString("test_read", "reading things") .addString("test_write", "creating things") .addString("test_1", "deleting things") ) ) ) ) .addSecuritySchemes(SECURITY_SCHEME_BEARER, new SecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme("bearer") .description("provide a valid access token") ) ; }; } /** * Display a hint in the log. */ @EventListener public void onApplicationEvent(ServletWebServerInitializedEvent event) { log.info("**************************"); int port = event.getWebServer().getPort(); log.info("visit http://localhost:{}/swagger-ui.html for UI", port); log.info("visit http://localhost:{}/openapi.yaml for OpenAPI document", port); log.info("**************************"); } } movie-microservice/src/main/resources/application.yml +3 −1 Original line number Diff line number Diff line Loading @@ -7,6 +7,8 @@ server: include-message: always spring: profiles: active: prod # # secrets should not be stored here datasource: url: jdbc:postgresql://movie-db:5431/movie_database Loading movie-microservice/src/test/java/cz/muni/fi/iamdb/config/SecurityConfig.java 0 → 100644 +26 −0 Original line number Diff line number Diff line package cz.muni.fi.iamdb.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration @Profile("test") public class SecurityConfig { /** * Configure access restrictions to the API. * Introspection of opaque access token is configured, introspection endpoint is defined in application.yml. */ @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(x -> x .anyRequest().permitAll() ) ; return http.build(); } } movie-microservice/src/test/java/cz/muni/fi/iamdb/rest/MovieRestControllerIT.java +71 −71 Original line number Diff line number Diff line //package cz.muni.fi.iamdb.rest; // //import org.junit.jupiter.api.Test; //import org.springframework.beans.factory.annotation.Autowired; //import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; //import org.springframework.boot.test.context.SpringBootTest; //import org.springframework.test.context.ActiveProfiles; //import org.springframework.test.web.servlet.MockMvc; // //import java.util.UUID; // //import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; //import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; //import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; //import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; // //@SpringBootTest //@AutoConfigureMockMvc //@ActiveProfiles("test") //public class MovieRestControllerIT { // // @Autowired // private MockMvc mockMvc; // // // @Test // void testCreateAndFindById() throws Exception { // UUID id = UUID.randomUUID(); // String mockMovie = """ // { // "title": "title", // "duration": 20, // "description": "description", // "year_of_release": 1888, // "genre_ids": [], // "image_ids": [] // }"""; // mockMvc.perform(post("") // .contentType("application/json") // .content(mockMovie)) // .andExpect(status().isCreated()) // .andExpect(jsonPath("$.title").value("title")) // .andExpect(jsonPath("$.duration").value(20)) // .andExpect(jsonPath("$.description").value("description")); // } // // @Test // void testFindAll() throws Exception { // mockMvc.perform(get("")) // .andExpect(status().isOk()) // .andExpect(jsonPath("$").isArray()); // } // // @Test // void testCreateInvalid() throws Exception { // String mockMovie = """ // { // "title": "", // "duration": 0, // "description": "description", // "year_of_release": 1888, // "genre_ids": [], // "image_ids": [] // }"""; // mockMvc.perform(post("") // .contentType("application/json") // .content(mockMovie)) // .andExpect(status().isBadRequest()); // } // //} package cz.muni.fi.iamdb.rest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import java.util.UUID; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; @SpringBootTest @AutoConfigureMockMvc(addFilters = false) @ActiveProfiles("test") public class MovieRestControllerIT { @Autowired private MockMvc mockMvc; @Test void testCreateAndFindById() throws Exception { UUID id = UUID.randomUUID(); String mockMovie = """ { "title": "title", "duration": 20, "description": "description", "year_of_release": 1888, "genre_ids": [], "image_ids": [] }"""; mockMvc.perform(post("") .contentType("application/json") .content(mockMovie)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.title").value("title")) .andExpect(jsonPath("$.duration").value(20)) .andExpect(jsonPath("$.description").value("description")); } @Test void testFindAll() throws Exception { mockMvc.perform(get("")) .andExpect(status().isOk()) .andExpect(jsonPath("$").isArray()); } @Test void testCreateInvalid() throws Exception { String mockMovie = """ { "title": "", "duration": 0, "description": "description", "year_of_release": 1888, "genre_ids": [], "image_ids": [] }"""; mockMvc.perform(post("") .contentType("application/json") .content(mockMovie)) .andExpect(status().isBadRequest()); } } Loading
movie-microservice/src/main/java/cz/muni/fi/iamdb/MovieMicroservice.java +0 −122 Original line number Diff line number Diff line package cz.muni.fi.iamdb; import io.swagger.v3.oas.models.security.OAuthFlow; import io.swagger.v3.oas.models.security.OAuthFlows; import io.swagger.v3.oas.models.security.Scopes; import io.swagger.v3.oas.models.security.SecurityScheme; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springdoc.core.customizers.OpenApiCustomizer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent; import org.springframework.context.annotation.Bean; import org.springframework.context.event.EventListener; import org.springframework.http.HttpMethod; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; @SpringBootApplication public class MovieMicroservice { private static final Logger log = LoggerFactory.getLogger(MovieMicroservice.class); private static final String SECURITY_SCHEME_OAUTH2 = "MUNI"; private static final String SECURITY_SCHEME_BEARER = "Bearer"; public static void main(String[] args) { SpringApplication.run(MovieMicroservice.class, args); } /** * Configure access restrictions to the API. * Introspection of opaque access token is configured, introspection endpoint is defined in application.yml. */ @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { // todo extract from properties final String MOVIE_POST_SCOPE = "SCOPE_test_1"; final String MOVIE_GET_ALL_SCOPE = "SCOPE_test_2"; final String MOVIE_GET_SPECIFIC_SCOPE = "SCOPE_test_3"; final String MOVIE_PUT_SCOPE = "SCOPE_test_4"; final String MOVIE_DELETE_SCOPE = "SCOPE_test_5"; final String SPECIFIC_MOVIES_IMAGES_GET_SCOPE = "SCOPE_test_5"; // out of scopes :( final String IMAGES_POST_SCOPE = "SCOPE_test_1"; final String IMAGES_GET_ALL_SCOPE = "SCOPE_test_2"; final String IMAGES_GET_SPECIFIC_SCOPE = "SCOPE_test_3"; final String IMAGES_PUT_SCOPE = "SCOPE_test_4"; final String IMAGES_DELETE_SCOPE = "SCOPE_test_5"; final String GENRES_POST_SCOPE = "SCOPE_test_1"; final String GENRES_GET_ALL_SCOPE = "SCOPE_test_2"; final String GENRES_GET_SPECIFIC_SCOPE = "SCOPE_test_3"; final String GENRES_PUT_SCOPE = "SCOPE_test_4"; final String GENRES_DELETE_SCOPE = "SCOPE_test_5"; http .authorizeHttpRequests(x -> x .requestMatchers(HttpMethod.POST, "/").hasAuthority(MOVIE_POST_SCOPE) .requestMatchers(HttpMethod.GET, "/").hasAuthority(MOVIE_GET_ALL_SCOPE) .requestMatchers(HttpMethod.GET, "/{id}").hasAuthority(MOVIE_GET_SPECIFIC_SCOPE) .requestMatchers(HttpMethod.PUT, "/{id}").hasAuthority(MOVIE_PUT_SCOPE) .requestMatchers(HttpMethod.DELETE, "/{id}").hasAuthority(MOVIE_DELETE_SCOPE) .requestMatchers(HttpMethod.GET, "/{id}/images").hasAuthority(SPECIFIC_MOVIES_IMAGES_GET_SCOPE) // images .requestMatchers(HttpMethod.POST, "/images").hasAuthority(IMAGES_POST_SCOPE) .requestMatchers(HttpMethod.GET, "/images").hasAuthority(IMAGES_GET_ALL_SCOPE) .requestMatchers(HttpMethod.GET, "/images/{id}").hasAuthority(IMAGES_GET_SPECIFIC_SCOPE) .requestMatchers(HttpMethod.PUT, "/images/{id}").hasAuthority(IMAGES_PUT_SCOPE) .requestMatchers(HttpMethod.DELETE, "/images/{id}").hasAuthority(IMAGES_DELETE_SCOPE) // genres .requestMatchers(HttpMethod.POST, "/genres").hasAuthority(GENRES_POST_SCOPE) .requestMatchers(HttpMethod.GET, "/genres").hasAuthority(GENRES_GET_ALL_SCOPE) .requestMatchers(HttpMethod.GET, "/genres/{id}").hasAuthority(GENRES_GET_SPECIFIC_SCOPE) .requestMatchers(HttpMethod.PUT, "/genres/{id}").hasAuthority(GENRES_PUT_SCOPE) .requestMatchers(HttpMethod.DELETE, "/genres/{id}").hasAuthority(GENRES_DELETE_SCOPE) // defensively deny all other requests // todo allow the seeding endpoints (or refactor the seeder) .anyRequest().denyAll() ) .oauth2ResourceServer(oauth2 -> oauth2.opaqueToken(Customizer.withDefaults())) ; return http.build(); } /** * Add security definitions to generated openapi.yaml. */ @Bean public OpenApiCustomizer openAPICustomizer() { return openApi -> { log.info("adding security to OpenAPI description"); openApi.getComponents() .addSecuritySchemes(SECURITY_SCHEME_OAUTH2, new SecurityScheme() .type(SecurityScheme.Type.OAUTH2) .description("get access token with OAuth 2 Authorization Code Grant") .flows(new OAuthFlows() .authorizationCode(new OAuthFlow() .authorizationUrl("https://oidc.muni.cz/oidc/authorize") .tokenUrl("https://oidc.muni.cz/oidc/token") .scopes(new Scopes() .addString("test_read", "reading things") .addString("test_write", "creating things") .addString("test_1", "deleting things") ) ) ) ) .addSecuritySchemes(SECURITY_SCHEME_BEARER, new SecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme("bearer") .description("provide a valid access token") ) ; }; } /** * Display a hint in the log. */ @EventListener public void onApplicationEvent(ServletWebServerInitializedEvent event) { log.info("**************************"); int port = event.getWebServer().getPort(); log.info("visit http://localhost:{}/swagger-ui.html for UI", port); log.info("visit http://localhost:{}/openapi.yaml for OpenAPI document", port); log.info("**************************"); } }
movie-microservice/src/main/java/cz/muni/fi/iamdb/config/SecurityConfig.java 0 → 100644 +129 −0 Original line number Diff line number Diff line package cz.muni.fi.iamdb.config; import cz.muni.fi.iamdb.MovieMicroservice; import io.swagger.v3.oas.models.security.OAuthFlow; import io.swagger.v3.oas.models.security.OAuthFlows; import io.swagger.v3.oas.models.security.Scopes; import io.swagger.v3.oas.models.security.SecurityScheme; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springdoc.core.customizers.OpenApiCustomizer; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent; import org.springframework.context.annotation.Bean; import org.springframework.context.event.EventListener; import org.springframework.http.HttpMethod; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; @SpringBootApplication public class SecurityConfig { private static final Logger log = LoggerFactory.getLogger(MovieMicroservice.class); private static final String SECURITY_SCHEME_OAUTH2 = "MUNI"; private static final String SECURITY_SCHEME_BEARER = "Bearer"; /** * Configure access restrictions to the API. * Introspection of opaque access token is configured, introspection endpoint is defined in application.yml. */ @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { // todo extract from properties final String MOVIE_POST_SCOPE = "SCOPE_test_1"; final String MOVIE_GET_ALL_SCOPE = "SCOPE_test_2"; final String MOVIE_GET_SPECIFIC_SCOPE = "SCOPE_test_3"; final String MOVIE_PUT_SCOPE = "SCOPE_test_4"; final String MOVIE_DELETE_SCOPE = "SCOPE_test_5"; final String SPECIFIC_MOVIES_IMAGES_GET_SCOPE = "SCOPE_test_5"; // out of scopes :( final String IMAGES_POST_SCOPE = "SCOPE_test_1"; final String IMAGES_GET_ALL_SCOPE = "SCOPE_test_2"; final String IMAGES_GET_SPECIFIC_SCOPE = "SCOPE_test_3"; final String IMAGES_PUT_SCOPE = "SCOPE_test_4"; final String IMAGES_DELETE_SCOPE = "SCOPE_test_5"; final String GENRES_POST_SCOPE = "SCOPE_test_1"; final String GENRES_GET_ALL_SCOPE = "SCOPE_test_2"; final String GENRES_GET_SPECIFIC_SCOPE = "SCOPE_test_3"; final String GENRES_PUT_SCOPE = "SCOPE_test_4"; final String GENRES_DELETE_SCOPE = "SCOPE_test_5"; http .authorizeHttpRequests(x -> x .requestMatchers(HttpMethod.POST, "/").hasAuthority(MOVIE_POST_SCOPE) .requestMatchers(HttpMethod.GET, "/").hasAuthority(MOVIE_GET_ALL_SCOPE) .requestMatchers(HttpMethod.GET, "/{id}").hasAuthority(MOVIE_GET_SPECIFIC_SCOPE) .requestMatchers(HttpMethod.PUT, "/{id}").hasAuthority(MOVIE_PUT_SCOPE) .requestMatchers(HttpMethod.DELETE, "/{id}").hasAuthority(MOVIE_DELETE_SCOPE) .requestMatchers(HttpMethod.GET, "/{id}/images").hasAuthority(SPECIFIC_MOVIES_IMAGES_GET_SCOPE) // images .requestMatchers(HttpMethod.POST, "/images").hasAuthority(IMAGES_POST_SCOPE) .requestMatchers(HttpMethod.GET, "/images").hasAuthority(IMAGES_GET_ALL_SCOPE) .requestMatchers(HttpMethod.GET, "/images/{id}").hasAuthority(IMAGES_GET_SPECIFIC_SCOPE) .requestMatchers(HttpMethod.PUT, "/images/{id}").hasAuthority(IMAGES_PUT_SCOPE) .requestMatchers(HttpMethod.DELETE, "/images/{id}").hasAuthority(IMAGES_DELETE_SCOPE) // genres .requestMatchers(HttpMethod.POST, "/genres").hasAuthority(GENRES_POST_SCOPE) .requestMatchers(HttpMethod.GET, "/genres").hasAuthority(GENRES_GET_ALL_SCOPE) .requestMatchers(HttpMethod.GET, "/genres/{id}").hasAuthority(GENRES_GET_SPECIFIC_SCOPE) .requestMatchers(HttpMethod.PUT, "/genres/{id}").hasAuthority(GENRES_PUT_SCOPE) .requestMatchers(HttpMethod.DELETE, "/genres/{id}").hasAuthority(GENRES_DELETE_SCOPE) // defensively deny all other requests // todo allow the seeding endpoints (or refactor the seeder) .anyRequest().denyAll() ) .oauth2ResourceServer(oauth2 -> oauth2.opaqueToken(Customizer.withDefaults())) ; return http.build(); } /** * Add security definitions to generated openapi.yaml. */ @Bean public OpenApiCustomizer openAPICustomizer() { return openApi -> { log.info("adding security to OpenAPI description"); openApi.getComponents() .addSecuritySchemes(SECURITY_SCHEME_OAUTH2, new SecurityScheme() .type(SecurityScheme.Type.OAUTH2) .description("get access token with OAuth 2 Authorization Code Grant") .flows(new OAuthFlows() .authorizationCode(new OAuthFlow() .authorizationUrl("https://oidc.muni.cz/oidc/authorize") .tokenUrl("https://oidc.muni.cz/oidc/token") .scopes(new Scopes() .addString("test_read", "reading things") .addString("test_write", "creating things") .addString("test_1", "deleting things") ) ) ) ) .addSecuritySchemes(SECURITY_SCHEME_BEARER, new SecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme("bearer") .description("provide a valid access token") ) ; }; } /** * Display a hint in the log. */ @EventListener public void onApplicationEvent(ServletWebServerInitializedEvent event) { log.info("**************************"); int port = event.getWebServer().getPort(); log.info("visit http://localhost:{}/swagger-ui.html for UI", port); log.info("visit http://localhost:{}/openapi.yaml for OpenAPI document", port); log.info("**************************"); } }
movie-microservice/src/main/resources/application.yml +3 −1 Original line number Diff line number Diff line Loading @@ -7,6 +7,8 @@ server: include-message: always spring: profiles: active: prod # # secrets should not be stored here datasource: url: jdbc:postgresql://movie-db:5431/movie_database Loading
movie-microservice/src/test/java/cz/muni/fi/iamdb/config/SecurityConfig.java 0 → 100644 +26 −0 Original line number Diff line number Diff line package cz.muni.fi.iamdb.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration @Profile("test") public class SecurityConfig { /** * Configure access restrictions to the API. * Introspection of opaque access token is configured, introspection endpoint is defined in application.yml. */ @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(x -> x .anyRequest().permitAll() ) ; return http.build(); } }
movie-microservice/src/test/java/cz/muni/fi/iamdb/rest/MovieRestControllerIT.java +71 −71 Original line number Diff line number Diff line //package cz.muni.fi.iamdb.rest; // //import org.junit.jupiter.api.Test; //import org.springframework.beans.factory.annotation.Autowired; //import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; //import org.springframework.boot.test.context.SpringBootTest; //import org.springframework.test.context.ActiveProfiles; //import org.springframework.test.web.servlet.MockMvc; // //import java.util.UUID; // //import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; //import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; //import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; //import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; // //@SpringBootTest //@AutoConfigureMockMvc //@ActiveProfiles("test") //public class MovieRestControllerIT { // // @Autowired // private MockMvc mockMvc; // // // @Test // void testCreateAndFindById() throws Exception { // UUID id = UUID.randomUUID(); // String mockMovie = """ // { // "title": "title", // "duration": 20, // "description": "description", // "year_of_release": 1888, // "genre_ids": [], // "image_ids": [] // }"""; // mockMvc.perform(post("") // .contentType("application/json") // .content(mockMovie)) // .andExpect(status().isCreated()) // .andExpect(jsonPath("$.title").value("title")) // .andExpect(jsonPath("$.duration").value(20)) // .andExpect(jsonPath("$.description").value("description")); // } // // @Test // void testFindAll() throws Exception { // mockMvc.perform(get("")) // .andExpect(status().isOk()) // .andExpect(jsonPath("$").isArray()); // } // // @Test // void testCreateInvalid() throws Exception { // String mockMovie = """ // { // "title": "", // "duration": 0, // "description": "description", // "year_of_release": 1888, // "genre_ids": [], // "image_ids": [] // }"""; // mockMvc.perform(post("") // .contentType("application/json") // .content(mockMovie)) // .andExpect(status().isBadRequest()); // } // //} package cz.muni.fi.iamdb.rest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import java.util.UUID; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; @SpringBootTest @AutoConfigureMockMvc(addFilters = false) @ActiveProfiles("test") public class MovieRestControllerIT { @Autowired private MockMvc mockMvc; @Test void testCreateAndFindById() throws Exception { UUID id = UUID.randomUUID(); String mockMovie = """ { "title": "title", "duration": 20, "description": "description", "year_of_release": 1888, "genre_ids": [], "image_ids": [] }"""; mockMvc.perform(post("") .contentType("application/json") .content(mockMovie)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.title").value("title")) .andExpect(jsonPath("$.duration").value(20)) .andExpect(jsonPath("$.description").value("description")); } @Test void testFindAll() throws Exception { mockMvc.perform(get("")) .andExpect(status().isOk()) .andExpect(jsonPath("$").isArray()); } @Test void testCreateInvalid() throws Exception { String mockMovie = """ { "title": "", "duration": 0, "description": "description", "year_of_release": 1888, "genre_ids": [], "image_ids": [] }"""; mockMvc.perform(post("") .contentType("application/json") .content(mockMovie)) .andExpect(status().isBadRequest()); } }