diff --git a/README.md b/README.md index 12c3a9e9..957af0ef 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ docker-compose -p common-api-development -f docker-compose.dev.yml up -d Building image for production ```bash cd docker -DOCKER_BUILDKIT=1 docker build -f Dockerfile.prod -t common-api:4.1.1 ../ +DOCKER_BUILDKIT=1 docker build -f Dockerfile.prod -t common-api:4.1.2 ../ ``` docker compose for production @@ -155,13 +155,15 @@ docker-compose -p common-api -f docker-compose.prod.yml up -d | SMTP server password | `SMTP_PASSWORD` | secret | | time for recovery email to expire | `MINUTES_TO_EXPIRE_RECOVERY_CODE` | 20 | | max requests per minute | `MAX_REQUESTS_PER_MINUTE` | 10 | +| swagger username | `SWAGGER_USERNAME` | `null` | +| swagger password | `SWAGGER_PASSWORD` | `null` | > these variables are defined in: [**application.properties**](./src/main/resources/application.properties) > > ```shell > # to change the value of some environment variable at runtime > # on execution, just pass it as a parameter. (like --SERVER_PORT=80). -> $ java -jar api-4.1.1.RELEASE.jar --SERVER_PORT=80 +> $ java -jar api-4.1.2.RELEASE.jar --SERVER_PORT=80 > ``` diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index b1276730..f3c5afcf 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -17,7 +17,7 @@ services: - ./.volumes/database:/var/lib/postgresql/data api: - image: common-api:4.1.1 + image: common-api:4.1.2 restart: unless-stopped container_name: common-api links: @@ -30,4 +30,5 @@ services: DB_PASSWORD: ${DB_PASSWORD} TOKEN_SECRET: ${TOKEN_SECRET} DB_SHOW_SQL: "false" - PRIVATE_SWAGGER: "true" + SWAGGER_USERNAME: "example" + SWAGGER_PASSWORD: "example" diff --git a/pom.xml b/pom.xml index 285bbef7..b6846f5d 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ com.github.throyer.common.spring-boot api - 4.1.1 + 4.1.2 CRUD API Exemplo de api simples com Spring Boot diff --git a/src/main/java/com/github/throyer/common/springboot/configurations/SpringSecurityConfiguration.java b/src/main/java/com/github/throyer/common/springboot/configurations/SpringSecurityConfiguration.java index ffc2f0e3..e5f160aa 100644 --- a/src/main/java/com/github/throyer/common/springboot/configurations/SpringSecurityConfiguration.java +++ b/src/main/java/com/github/throyer/common/springboot/configurations/SpringSecurityConfiguration.java @@ -2,26 +2,30 @@ import static com.github.throyer.common.springboot.constants.SECURITY.ACESSO_NEGADO_URL; import static com.github.throyer.common.springboot.constants.SECURITY.DAY_MILLISECONDS; +import static com.github.throyer.common.springboot.constants.SECURITY.ENCODER; import static com.github.throyer.common.springboot.constants.SECURITY.HOME_URL; import static com.github.throyer.common.springboot.constants.SECURITY.LOGIN_ERROR_URL; import static com.github.throyer.common.springboot.constants.SECURITY.LOGIN_URL; import static com.github.throyer.common.springboot.constants.SECURITY.LOGOUT_URL; -import static com.github.throyer.common.springboot.constants.SECURITY.PASSWORD_ENCODER; import static com.github.throyer.common.springboot.constants.SECURITY.PASSWORD_PARAMETER; -import static com.github.throyer.common.springboot.constants.SECURITY.PRIVATE_SWAGGER; -import static com.github.throyer.common.springboot.constants.SECURITY.PUBLIC_API_ROUTES; +import static com.github.throyer.common.springboot.constants.SECURITY.PUBLICS; import static com.github.throyer.common.springboot.constants.SECURITY.SESSION_COOKIE_NAME; import static com.github.throyer.common.springboot.constants.SECURITY.TOKEN_SECRET; import static com.github.throyer.common.springboot.constants.SECURITY.USERNAME_PARAMETER; import static com.github.throyer.common.springboot.utils.Responses.forbidden; +import static java.util.Optional.ofNullable; import static org.springframework.http.HttpMethod.GET; import static org.springframework.http.HttpMethod.POST; import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; +import java.util.Optional; +import java.util.stream.Stream; + import com.github.throyer.common.springboot.domain.session.service.SessionService; import com.github.throyer.common.springboot.middlewares.AuthorizationMiddleware; import org.springframework.beans.factory.annotation.Autowired; +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; @@ -46,6 +50,9 @@ public class SpringSecurityConfiguration { private final SessionService sessionService; private final AuthorizationMiddleware filter; + public static String SWAGGER_USERNAME; + public static String SWAGGER_PASSWORD; + @Autowired public SpringSecurityConfiguration( SessionService sessionService, @@ -57,11 +64,29 @@ public SpringSecurityConfiguration( @Autowired protected void globalConfiguration( - AuthenticationManagerBuilder authentication + AuthenticationManagerBuilder authentication, + @Value("${swagger.username}") String username, + @Value("${swagger.password}") String password ) throws Exception { + SpringSecurityConfiguration.SWAGGER_USERNAME = username; + SpringSecurityConfiguration.SWAGGER_PASSWORD = password; + + if (Stream + .of(ofNullable(SWAGGER_PASSWORD), ofNullable(SWAGGER_USERNAME)) + .allMatch(Optional::isPresent)) { + + authentication + .inMemoryAuthentication() + .passwordEncoder(ENCODER) + .withUser(username) + .password(ENCODER.encode(password)) + .authorities("SWAGGER"); + } + + authentication .userDetailsService(sessionService) - .passwordEncoder(PASSWORD_ENCODER); + .passwordEncoder(ENCODER); } @Bean @@ -74,7 +99,7 @@ public AuthenticationManager authenticationManager( @Bean @Order(1) public SecurityFilterChain api(HttpSecurity http) throws Exception { - PUBLIC_API_ROUTES.injectOn(http); + PUBLICS.injectOn(http); http .antMatcher("/api/**") @@ -137,19 +162,20 @@ public SecurityFilterChain app(HttpSecurity http) throws Exception { @Bean @Order(4) public SecurityFilterChain swagger(HttpSecurity http) throws Exception { + if (Stream + .of(ofNullable(SWAGGER_PASSWORD), ofNullable(SWAGGER_USERNAME)) + .allMatch(Optional::isPresent)) { - if (PRIVATE_SWAGGER) { http - .authorizeRequests() - .antMatchers("/swagger-ui/**", "/swagger-ui.html", "/**.html", "/documentation/**") + .antMatcher("/swagger-ui/**") + .authorizeRequests() + .anyRequest() .authenticated() + .and() + .sessionManagement() + .sessionCreationPolicy(STATELESS) .and() .httpBasic(); - } else { - http - .authorizeRequests() - .antMatchers("/swagger-ui/**", "/swagger-ui.html", "/**.html", "/documentation/**") - .permitAll(); } return http.build(); diff --git a/src/main/java/com/github/throyer/common/springboot/constants/SECURITY.java b/src/main/java/com/github/throyer/common/springboot/constants/SECURITY.java index 93412d44..108c36b7 100644 --- a/src/main/java/com/github/throyer/common/springboot/constants/SECURITY.java +++ b/src/main/java/com/github/throyer/common/springboot/constants/SECURITY.java @@ -19,17 +19,15 @@ public SECURITY( @Value("${token.secret}") String tokenSecret, @Value("${token.expiration-in-hours}") Integer tokenExpirationInHours, @Value("${token.refresh.expiration-in-days}") Integer refreshTokenExpirationInDays, - @Value("${server.servlet.session.cookie.name}") String sessionCookieName, - @Value("${swagger.is-private}") Boolean privateSwagger + @Value("${server.servlet.session.cookie.name}") String sessionCookieName ) { SECURITY.TOKEN_SECRET = tokenSecret; SECURITY.TOKEN_EXPIRATION_IN_HOURS = tokenExpirationInHours; SECURITY.REFRESH_TOKEN_EXPIRATION_IN_DAYS = refreshTokenExpirationInDays; SECURITY.SESSION_COOKIE_NAME = sessionCookieName; - SECURITY.PRIVATE_SWAGGER = privateSwagger; } - public static final PublicRoutes PUBLIC_API_ROUTES = create() + public static final PublicRoutes PUBLICS = create() .add(GET, "/api") .add(POST, "/api/users", "/api/sessions/**", "/api/recoveries/**"); @@ -37,7 +35,7 @@ public SECURITY( public static final JsonWebToken JWT = new JsonWebToken(); public static final Integer PASSWORD_STRENGTH = 10; - public static final BCryptPasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(PASSWORD_STRENGTH); + public static final BCryptPasswordEncoder ENCODER = new BCryptPasswordEncoder(PASSWORD_STRENGTH); public static final String ROLES_KEY_ON_JWT = "roles"; @@ -46,7 +44,6 @@ public SECURITY( public static Integer REFRESH_TOKEN_EXPIRATION_IN_DAYS; public static String SESSION_COOKIE_NAME; - public static Boolean PRIVATE_SWAGGER; public static final String USERNAME_PARAMETER = "email"; public static final String PASSWORD_PARAMETER = "password"; diff --git a/src/main/java/com/github/throyer/common/springboot/domain/session/service/SessionService.java b/src/main/java/com/github/throyer/common/springboot/domain/session/service/SessionService.java index 647c2e8c..97d41976 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/session/service/SessionService.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/session/service/SessionService.java @@ -43,7 +43,7 @@ public static void authorize( HttpServletRequest request, HttpServletResponse response ) { - if (PUBLIC_API_ROUTES.anyMatch(request)) { + if (PUBLICS.anyMatch(request)) { return; } diff --git a/src/main/java/com/github/throyer/common/springboot/domain/user/entity/User.java b/src/main/java/com/github/throyer/common/springboot/domain/user/entity/User.java index 78deddbf..6101927c 100644 --- a/src/main/java/com/github/throyer/common/springboot/domain/user/entity/User.java +++ b/src/main/java/com/github/throyer/common/springboot/domain/user/entity/User.java @@ -21,7 +21,7 @@ import java.util.Objects; import static com.fasterxml.jackson.annotation.JsonProperty.Access.WRITE_ONLY; -import static com.github.throyer.common.springboot.constants.SECURITY.PASSWORD_ENCODER; +import static com.github.throyer.common.springboot.constants.SECURITY.ENCODER; import static com.github.throyer.common.springboot.domain.management.repository.Queries.NON_DELETED_CLAUSE; import static com.github.throyer.common.springboot.utils.JSON.stringify; import static java.util.Objects.hash; @@ -120,17 +120,16 @@ public void merge(UpdateUserProps props) { } public void updatePassword(String newPassword) { - this.password = PASSWORD_ENCODER - .encode(newPassword); + this.password = ENCODER.encode(newPassword); } public Boolean validatePassword(String password) { - return PASSWORD_ENCODER.matches(password, this.password); + return ENCODER.matches(password, this.password); } @PrePersist private void created() { - this.password = PASSWORD_ENCODER.encode(password); + this.password = ENCODER.encode(password); } @Override diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 08e17559..381f25d3 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -23,8 +23,6 @@ spring.h2.console.enabled=false spring.jpa.open-in-view=false # swagger -springdoc.swagger-ui.path=/documentation -springdoc.api-docs.path=/documentation/schemas springdoc.default-produces-media-type=application/json springdoc.default-consumes-media-type=application/json @@ -33,7 +31,9 @@ token.expiration-in-hours=${TOKEN_EXPIRATION_IN_HOURS:24} token.refresh.expiration-in-days=${REFRESH_TOKEN_EXPIRATION_IN_DAYS:7} token.secret=${TOKEN_SECRET:secret} server.servlet.session.cookie.name=API_EXAMPLE_SESSION_ID -swagger.is-private=${PRIVATE_SWAGGER:true} +server.servlet.session.cookie.path=/app +swagger.username=${SWAGGER_USERNAME:#{null}} +swagger.password=${SWAGGER_PASSWORD:#{null}} # smtp configurations spring.mail.host=${SMTP_HOST:smtp.gmail.com} diff --git a/src/test/java/com/github/throyer/common/springboot/swagger/SwaggerAuthTests.java b/src/test/java/com/github/throyer/common/springboot/swagger/SwaggerAuthTests.java new file mode 100644 index 00000000..8a3ab0af --- /dev/null +++ b/src/test/java/com/github/throyer/common/springboot/swagger/SwaggerAuthTests.java @@ -0,0 +1,45 @@ +package com.github.throyer.common.springboot.swagger; + +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.DisplayName; +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.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest(webEnvironment = WebEnvironment.MOCK) +@AutoConfigureMockMvc +@TestPropertySource(properties = { + "SWAGGER_USERNAME=test", + "SWAGGER_PASSWORD=test"}) +public class SwaggerAuthTests { + + @Autowired + private MockMvc api; + + @Test + @DisplayName("Deve exibir a documentação com basic auth valido") + public void should_display_the_swagger_docs_ui_with_valid_credentials() throws Exception { + + var request = get("/swagger-ui/index.html?configUrl=/documentation/schemas/swagger-config") + .header(AUTHORIZATION, "Basic dGVzdDp0ZXN0"); + + api.perform(request) + .andExpect(status().isOk()); + } + @Test + @DisplayName("Não deve exibir a documentação com basic auth invalido") + public void must_not_display_the_swagger_docs_ui_with_invalid_credentials() throws Exception { + var request = get("/swagger-ui/index.html?configUrl=/documentation/schemas/swagger-config") + .header(AUTHORIZATION, "Basic anViaWxldTppcmluZXU="); + + api.perform(request) + .andExpect(status().isUnauthorized()); + } +} \ No newline at end of file diff --git a/src/test/java/com/github/throyer/common/springboot/SwaggerTests.java b/src/test/java/com/github/throyer/common/springboot/swagger/SwaggerTests.java similarity index 95% rename from src/test/java/com/github/throyer/common/springboot/SwaggerTests.java rename to src/test/java/com/github/throyer/common/springboot/swagger/SwaggerTests.java index c7ff00c1..c1fbca24 100644 --- a/src/test/java/com/github/throyer/common/springboot/SwaggerTests.java +++ b/src/test/java/com/github/throyer/common/springboot/swagger/SwaggerTests.java @@ -1,4 +1,4 @@ -package com.github.throyer.common.springboot; +package com.github.throyer.common.springboot.swagger; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 72e1da1e..8a32687b 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -26,7 +26,8 @@ token.expiration-in-hours=24 token.refresh.expiration-in-days=7 token.secret=secret server.servlet.session.cookie.name=JSESSIONID -swagger.is-private=false +swagger.username=${SWAGGER_USERNAME:#{null}} +swagger.password=${SWAGGER_PASSWORD:#{null}} # recovery email recovery.minutes-to-expire=20