diff --git a/.env.example b/.env.example index f4ff381..ec2f963 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,5 @@ SPRING_DATASOURCE_USERNAME=root SPRING_DATASOURCE_PASSWORD=changeme APP_PORT=80 +ADMIN_USER_NAME=admin +ADMIN_USER_HASHED_PASSWORD=changeme \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c88c7dc..783dc82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,4 +66,6 @@ jobs: SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: rootpassword SERVER_PORT: 8080 + ADMIN_USER_NAME: admin + ADMIN_USER_HASHED_PASSWORD: admin run: ./mvnw test -B diff --git a/docker-compose.yml b/docker-compose.yml index 0fb70f1..e5ec01a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,8 @@ services: SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/healthiermo?useSSL=true&useLegacyDatetimeCode=false&serverTimezone=UTC SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME} SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD} + ADMIN_USER_NAME: ${ADMIN_USER_NAME} + ADMIN_USER_HASHED_PASSWORD: ${ADMIN_USER_HASHED_PASSWORD} depends_on: db: condition: service_healthy diff --git a/pom.xml b/pom.xml index a21bdc9..dd1685a 100644 --- a/pom.xml +++ b/pom.xml @@ -53,6 +53,10 @@ mysql-connector-j runtime + + org.springframework.boot + spring-boot-starter-security + org.springframework.boot spring-boot-starter-webmvc-test diff --git a/src/main/java/org/healthiermo/homepage/config/SecurityConfig.java b/src/main/java/org/healthiermo/homepage/config/SecurityConfig.java new file mode 100644 index 0000000..1316ebd --- /dev/null +++ b/src/main/java/org/healthiermo/homepage/config/SecurityConfig.java @@ -0,0 +1,57 @@ +package org.healthiermo.homepage.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.AbstractHttpConfigurer; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +import static org.springframework.security.config.Customizer.withDefaults; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Value("${spring.security.user.name}") + private String adminUsername; + + @Value("${spring.security.user.password}") + private String adminPassword ; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(auth -> auth + .requestMatchers("/", "/index", "/*.png", "/*.js", "/tingle-master/**").permitAll() + .anyRequest().authenticated() + ) + .httpBasic(withDefaults()) + .csrf(AbstractHttpConfigurer::disable); + + return http.build(); + } + + @Bean + public UserDetailsService x() { + UserDetails user = User.builder() + .username(this.adminUsername) + .password(this.adminPassword) + .roles("USER") + .build(); + + return new InMemoryUserDetailsManager(user); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 0ee201b..73b4c84 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -12,6 +12,10 @@ spring: url: jdbc:mysql://localhost:3306/healthiermo?useSSL=true&useLegacyDatetimeCode=false&serverTimezone=UTC username: ${SPRING_DATASOURCE_USERNAME} password: ${SPRING_DATASOURCE_PASSWORD} + security: + user: + name: ${ADMIN_USER_NAME} + password: ${ADMIN_USER_HASHED_PASSWORD} server: port: 80 tomcat: diff --git a/src/test/java/org/healthiermo/homepage/FileUploadIntegrationTest.java b/src/test/java/org/healthiermo/homepage/FileUploadIntegrationTest.java index f6b7575..fea185e 100644 --- a/src/test/java/org/healthiermo/homepage/FileUploadIntegrationTest.java +++ b/src/test/java/org/healthiermo/homepage/FileUploadIntegrationTest.java @@ -9,6 +9,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.beans.factory.annotation.Value; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.springframework.util.LinkedMultiValueMap; @@ -32,9 +33,17 @@ class FileUploadIntegrationTest { @LocalServerPort private int port; + @Value("${spring.security.user.name}") + private String username; + + @Value("${spring.security.user.password}") + private String password; + @DynamicPropertySource static void overrideProperties(DynamicPropertyRegistry registry) { registry.add("app.public-data-root", () -> tempDir.toString()); + registry.add("spring.security.user.name", () -> "testuser"); + registry.add("spring.security.user.password", () -> "testpass"); } @Test @@ -57,6 +66,7 @@ public String getFilename() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.MULTIPART_FORM_DATA); + headers.setBasicAuth(username, password); HttpEntity> request = new HttpEntity<>(body, headers); RestTemplate restTemplate = new RestTemplate(); diff --git a/src/test/java/org/healthiermo/homepage/config/SecurityConfigTest.java b/src/test/java/org/healthiermo/homepage/config/SecurityConfigTest.java new file mode 100644 index 0000000..cb8fb03 --- /dev/null +++ b/src/test/java/org/healthiermo/homepage/config/SecurityConfigTest.java @@ -0,0 +1,71 @@ +package org.healthiermo.homepage.config; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +import static org.junit.jupiter.api.Assertions.*; + + +@SpringBootTest +class SecurityConfigTest { + + @Autowired + private SecurityConfig config; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Autowired + private UserDetailsService userDetailsService; + + @DynamicPropertySource + static void overrideProperties(DynamicPropertyRegistry registry) { + registry.add("spring.security.user.name", () -> "admin"); + registry.add("spring.security.user.password", () -> "password"); + } + + @Test + void passwordEncoderShouldNotBeNull() { + assertNotNull(passwordEncoder); + } + + @Test + void passwordEncoderShouldEncodePasswords() { + String rawPassword = "password"; + String encodedPassword = passwordEncoder.encode(rawPassword); + + assertNotNull(encodedPassword); + assertNotEquals(rawPassword, encodedPassword); + assertTrue(passwordEncoder.matches(rawPassword, encodedPassword)); + } + + @Test + void userDetailsServiceShouldNotBeNull() { + assertNotNull(userDetailsService); + } + + @Test + void userDetailsServiceShouldLoadUserByUsername() { + UserDetails user = userDetailsService.loadUserByUsername("admin"); + + assertNotNull(user); + assertEquals("admin", user.getUsername()); + assertTrue(user.getAuthorities().stream() + .anyMatch(a -> a.getAuthority().equals("ROLE_USER"))); + } + + @Test + void userDetailsServiceShouldEncodePassword() { + UserDetails user = userDetailsService.loadUserByUsername("admin"); + + assertNotNull(user.getPassword()); + assertNotEquals("password", user.getPassword()); + assertTrue(passwordEncoder.matches("password", user.getPassword())); + } +}