Commit 49ad6f56 authored by Lukáš Chudíček's avatar Lukáš Chudíček
Browse files

test: tests working with security in movie

parent fa65507d
Loading
Loading
Loading
Loading
Loading
+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("**************************");
    }

}
+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("**************************");
    }

}
+3 −1
Original line number Diff line number Diff line
@@ -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
+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();
    }
}
+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