diff --git a/docker-compose.yml b/docker-compose.yml index df03d641e5990cd8b85c106b4ddd4ee0e8b16bea..5c03c4777defb8fbca4b7785258e9d94aa61f379 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,7 @@ volumes: networks: public: - name: core + name: public driver: bridge ipam: config: @@ -23,7 +23,7 @@ networks: config: - subnet: 172.28.0.0/16 core: - name: public + name: core driver: bridge ipam: config: @@ -361,6 +361,7 @@ services: image: fda-ui networks: core: + public: ports: - "3000:3000" env_file: diff --git a/fda-authentication-service/rest-service/src/main/java/at/tuwien/endpoints/TokenEndpoint.java b/fda-authentication-service/rest-service/src/main/java/at/tuwien/endpoints/TokenEndpoint.java index 0fed6c2a5aa62ac0140b23d4e4aca73549725555..d72d0d3eced4ee5881d3f105b4be35e92af3c22c 100644 --- a/fda-authentication-service/rest-service/src/main/java/at/tuwien/endpoints/TokenEndpoint.java +++ b/fda-authentication-service/rest-service/src/main/java/at/tuwien/endpoints/TokenEndpoint.java @@ -81,16 +81,17 @@ public class TokenEndpoint { .body(dto); } - @DeleteMapping("/{hash}") + @DeleteMapping("/{id}") @Transactional @Timed(value = "token.delete", description = "Time needed to delete the developer tokens") @Operation(summary = "Delete developer token", security = @SecurityRequirement(name = "bearerAuth")) - public void delete(@NotNull @PathVariable("hash") String hash, + public void delete(@NotNull @PathVariable("id") Long id, @NotNull Principal principal) throws TokenNotFoundException, UserNotFoundException { - log.debug("endpoint delete developer token, hash={}, principal={}", hash, principal); - final Token token = tokenService.findOne(hash); + log.debug("endpoint delete developer token, id={}, principal={}", id, principal); + final Token token = tokenService.findOne(id); log.trace("found token {}", token); tokenService.delete(token.getTokenHash(), principal); + log.info("Deleted token with id {}", id); } } \ No newline at end of file diff --git a/fda-authentication-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java b/fda-authentication-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java index b228f91cb143ea25252ba1bb2e2dc248c633c0f1..e3749b24406f4121898982f388777574f753740d 100644 --- a/fda-authentication-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java +++ b/fda-authentication-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java @@ -1,13 +1,19 @@ package at.tuwien; -import at.tuwien.api.auth.SignupRequestDto; +import at.tuwien.api.user.UserDetailsDto; +import at.tuwien.entities.user.RoleType; import at.tuwien.entities.user.TimeSecret; import at.tuwien.entities.user.Token; import at.tuwien.entities.user.User; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.test.context.TestPropertySource; +import java.security.Principal; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.List; @TestPropertySource(locations = "classpath:application.properties") public abstract class BaseUnitTest { @@ -38,9 +44,21 @@ public abstract class BaseUnitTest { .emailVerified(USER_1_VERIFIED) .themeDark(USER_1_THEME_DARK) .created(USER_1_CREATED) + .roles(List.of(RoleType.ROLE_RESEARCHER)) .lastModified(USER_1_LAST_MODIFIED) .build(); + public final static UserDetails USER_1_DETAILS = UserDetailsDto.builder() + .username(USER_1_USERNAME) + .email(USER_1_EMAIL) + .password(USER_1_PASSWORD) + .authorities(List.of(new SimpleGrantedAuthority("ROLE_RESEARCHER"))) + .build(); + + public final static Principal USER_1_PRINCIPAL = new UsernamePasswordAuthenticationToken(USER_1_DETAILS, + USER_1_PASSWORD, USER_1_DETAILS.getAuthorities()); + + public final static Long USER_2_ID = 2L; public final static String USER_2_EMAIL = "jane.doe@example.com"; public final static String USER_2_USERNAME = "jdoe2"; @@ -59,37 +77,64 @@ public abstract class BaseUnitTest { .emailVerified(USER_2_VERIFIED) .themeDark(USER_2_THEME_DARK) .created(USER_2_CREATED) + .roles(List.of(RoleType.ROLE_RESEARCHER)) .lastModified(USER_2_LAST_MODIFIED) .build(); - public final static Long TOKEN_1_ID = 1L; - public final static Boolean TOKEN_1_PROCESSED = false; - public final static String TOKEN_1_TOKEN = "mysecrettokenrandomlygenerated"; - public final static Instant TOKEN_1_VALID_TO = Instant.now() + public final static UserDetails USER_2_DETAILS = UserDetailsDto.builder() + .username(USER_2_USERNAME) + .email(USER_2_EMAIL) + .password(USER_2_PASSWORD) + .authorities(List.of(new SimpleGrantedAuthority("ROLE_RESEARCHER"))) + .build(); + + public final static Principal USER_2_PRINCIPAL = new UsernamePasswordAuthenticationToken(USER_2_DETAILS, + USER_2_PASSWORD, USER_2_DETAILS.getAuthorities()); + + public final static Long TIME_SECRET_1_ID = 1L; + public final static Boolean TIME_SECRET_1_PROCESSED = false; + public final static String TIME_SECRET_1_TOKEN = "mysecrettokenrandomlygenerated"; + public final static Instant TIME_SECRET_1_VALID_TO = Instant.now() .plus(1, ChronoUnit.DAYS); - public final static Long TOKEN_2_ID = 2L; - public final static Boolean TOKEN_2_PROCESSED = true; - public final static String TOKEN_2_TOKEN = "blahblahblah"; - public final static Instant TOKEN_2_VALID_TO = Instant.now() + public final static Long TIME_SECRET_2_ID = 2L; + public final static Boolean TIME_SECRET_2_PROCESSED = true; + public final static String TIME_SECRET_2_TOKEN = "blahblahblah"; + public final static Instant TIME_SECRET_2_VALID_TO = Instant.now() .plus(1, ChronoUnit.DAYS); - public final static TimeSecret TOKEN_1 = TimeSecret.builder() - .id(TOKEN_1_ID) + public final static TimeSecret TIME_SECRET_1 = TimeSecret.builder() + .id(TIME_SECRET_1_ID) .uid(USER_1_ID) .user(USER_1) - .token(TOKEN_1_TOKEN) - .processed(TOKEN_1_PROCESSED) - .validTo(TOKEN_1_VALID_TO) + .token(TIME_SECRET_1_TOKEN) + .processed(TIME_SECRET_1_PROCESSED) + .validTo(TIME_SECRET_1_VALID_TO) .build(); - public final static TimeSecret TOKEN_2 = TimeSecret.builder() - .id(TOKEN_2_ID) + public final static TimeSecret TIME_SECRET_2 = TimeSecret.builder() + .id(TIME_SECRET_2_ID) .uid(USER_2_ID) .user(USER_2) - .token(TOKEN_2_TOKEN) - .processed(TOKEN_2_PROCESSED) - .validTo(TOKEN_2_VALID_TO) + .token(TIME_SECRET_2_TOKEN) + .processed(TIME_SECRET_2_PROCESSED) + .validTo(TIME_SECRET_2_VALID_TO) + .build(); + + public final static Long TOKEN_1_ID = 1L; + public final static Instant TOKEN_1_EXPIRES = Instant.now().plus(100000000, ChronoUnit.MILLIS); + + public final static Token TOKEN_1 = Token.builder() + .id(TOKEN_1_ID) + .expires(TOKEN_1_EXPIRES) + .build(); + + public final static Long TOKEN_2_ID = 2L; + public final static Instant TOKEN_2_EXPIRES = Instant.now().plus(100000000, ChronoUnit.MILLIS); + + public final static Token TOKEN_2 = Token.builder() + .id(TOKEN_2_ID) + .expires(TOKEN_2_EXPIRES) .build(); } diff --git a/fda-authentication-service/rest-service/src/test/java/at/tuwien/config/H2Utils.java b/fda-authentication-service/rest-service/src/test/java/at/tuwien/config/H2Utils.java new file mode 100644 index 0000000000000000000000000000000000000000..dd93123161d36dbc189ed500e0a4d94c1d6b2fe8 --- /dev/null +++ b/fda-authentication-service/rest-service/src/test/java/at/tuwien/config/H2Utils.java @@ -0,0 +1,38 @@ +package at.tuwien.config; + +import lombok.SneakyThrows; +import lombok.extern.log4j.Log4j2; +import org.codehaus.plexus.util.FileUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityManager; +import java.io.File; +import java.io.IOException; + +@Log4j2 +@Component +public class H2Utils { + + @Autowired + private EntityManager entityManager; + + @Transactional + public void runQuery(String query) { + log.debug("query={}", query); + entityManager.createNativeQuery(query) + .executeUpdate(); + } + + @Transactional + public void runScript(String scriptName) { + try { + runQuery(FileUtils.fileRead(new File("./src/test/resources/" + scriptName))); + } catch (IOException e) { + log.error("Failed to load script {}", scriptName); + throw new RuntimeException("Failed to load script", e); + } + } + +} diff --git a/fda-authentication-service/rest-service/src/test/java/at/tuwien/service/AuthenticationServiceIntegrationTest.java b/fda-authentication-service/rest-service/src/test/java/at/tuwien/service/AuthenticationServiceIntegrationTest.java index e447b521ed9f2976d5d03d7faae512d7bb439b28..49608e0f1d2ab2a70c678c58042a9e4ba4d12dba 100644 --- a/fda-authentication-service/rest-service/src/test/java/at/tuwien/service/AuthenticationServiceIntegrationTest.java +++ b/fda-authentication-service/rest-service/src/test/java/at/tuwien/service/AuthenticationServiceIntegrationTest.java @@ -41,10 +41,10 @@ public class AuthenticationServiceIntegrationTest extends BaseUnitTest { public void beforeEach() { final User u1 = userRepository.save(USER_1); final User u2 = userRepository.save(USER_2); - TOKEN_1.setUser(u1); - tokenRepository.save(TOKEN_1); - TOKEN_2.setUser(u2); - tokenRepository.save(TOKEN_2); + TIME_SECRET_1.setUser(u1); + tokenRepository.save(TIME_SECRET_1); + TIME_SECRET_2.setUser(u2); + tokenRepository.save(TIME_SECRET_2); } @Test diff --git a/fda-authentication-service/rest-service/src/test/java/at/tuwien/service/TokenServiceIntegrationTest.java b/fda-authentication-service/rest-service/src/test/java/at/tuwien/service/TimeSecretUnitTest.java similarity index 80% rename from fda-authentication-service/rest-service/src/test/java/at/tuwien/service/TokenServiceIntegrationTest.java rename to fda-authentication-service/rest-service/src/test/java/at/tuwien/service/TimeSecretUnitTest.java index 1a2002e9fac1cb1af9cceb0e00cc27ef36a41574..b7b8d672ec2588d03966f619d04224c058a54574 100644 --- a/fda-authentication-service/rest-service/src/test/java/at/tuwien/service/TokenServiceIntegrationTest.java +++ b/fda-authentication-service/rest-service/src/test/java/at/tuwien/service/TimeSecretUnitTest.java @@ -21,16 +21,16 @@ import static org.junit.jupiter.api.Assertions.assertThrows; @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) @SpringBootTest @ExtendWith(SpringExtension.class) -public class TokenServiceIntegrationTest extends BaseUnitTest { +public class TimeSecretUnitTest extends BaseUnitTest { @MockBean private ReadyConfig readyConfig; @Autowired - private TimeSecretService tokenService; + private TimeSecretService timeSecretService; @Autowired - private TimeSecretRepository tokenRepository; + private TimeSecretRepository timeSecretRepository; @Autowired private UserRepository userRepository; @@ -38,16 +38,16 @@ public class TokenServiceIntegrationTest extends BaseUnitTest { @BeforeEach public void beforeEach() { userRepository.save(USER_1); - tokenRepository.save(TOKEN_1); + timeSecretRepository.save(TIME_SECRET_1); } @Test public void updateVerification_succeeds() throws SecretInvalidException { /* test */ - tokenService.invalidate(TOKEN_1_TOKEN); + timeSecretService.invalidate(TIME_SECRET_1_TOKEN); assertThrows(SecretInvalidException.class, () -> { - tokenService.invalidate(TOKEN_1_TOKEN); + timeSecretService.invalidate(TIME_SECRET_1_TOKEN); }); } diff --git a/fda-authentication-service/rest-service/src/test/java/at/tuwien/service/TokenIntegrationTest.java b/fda-authentication-service/rest-service/src/test/java/at/tuwien/service/TokenIntegrationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..239515c03a53448926dfb741ddc3d410472ac4a3 --- /dev/null +++ b/fda-authentication-service/rest-service/src/test/java/at/tuwien/service/TokenIntegrationTest.java @@ -0,0 +1,110 @@ +package at.tuwien.service; + +import at.tuwien.BaseUnitTest; +import at.tuwien.auth.JwtUtils; +import at.tuwien.config.H2Utils; +import at.tuwien.config.ReadyConfig; +import at.tuwien.entities.user.Token; +import at.tuwien.exception.UserNotFoundException; +import at.tuwien.repositories.TokenRepository; +import at.tuwien.repositories.UserRepository; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import javax.servlet.ServletException; + + +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Log4j2 +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +@SpringBootTest +@ExtendWith(SpringExtension.class) +public class TokenIntegrationTest extends BaseUnitTest { + + @MockBean + private ReadyConfig readyConfig; + + @Autowired + private TokenService tokenService; + + @Autowired + private UserRepository userRepository; + + @Autowired + private TokenRepository tokenRepository; + + @Autowired + private JwtUtils jwtUtils; + + @Autowired + private H2Utils h2Utils; + + @BeforeEach + public void beforeEach() { + userRepository.save(USER_1); + h2Utils.runScript("post-init.sql"); + } + + @Test + public void check_succeeds() throws ServletException { + + /* mock */ + final String jwt = jwtUtils.generateJwtToken(USER_1_USERNAME, TOKEN_1_EXPIRES); + final Token entity = Token.builder() + .token(jwt) + .tokenHash(JwtUtils.toHash(jwt)) + .creator(USER_1_ID) + .expires(TOKEN_1_EXPIRES) + .build(); + tokenRepository.save(entity) /* mock as invalid by the view script in ./resources/post-init.sql */; + final String jwt2 = jwtUtils.generateJwtToken(USER_1_USERNAME, TOKEN_2_EXPIRES); + final Token entity2 = Token.builder() + .token(jwt2) + .tokenHash(JwtUtils.toHash(jwt2)) + .creator(USER_1_ID) + .expires(TOKEN_2_EXPIRES) + .build(); + tokenRepository.save(entity2); + + /* test */ + tokenService.check(jwt2); + } + + @Test + public void check_revoked_fails() { + + /* mock */ + final String jwt = jwtUtils.generateJwtToken(USER_1_USERNAME, TOKEN_1_EXPIRES); + final Token entity = Token.builder() + .token(jwt) + .tokenHash(JwtUtils.toHash(jwt)) + .creator(USER_1_ID) + .expires(TOKEN_1_EXPIRES) + .build(); + final Token token = tokenRepository.save(entity); + tokenRepository.deleteById(token.getId()); + + /* test */ + assertThrows(ServletException.class, () -> { + tokenService.check(jwt); + }); + } + + @Test + public void create_userNotFound_fails() { + + /* test */ + assertThrows(UserNotFoundException.class, () -> { + tokenService.create(USER_2_PRINCIPAL); + }); + } + +} diff --git a/fda-authentication-service/rest-service/src/test/resources/application.properties b/fda-authentication-service/rest-service/src/test/resources/application.properties index 2c7e1df4ae863589b65f0d3de2b589254cac612f..c1c50277573f1ae1d4527c6f8bcc5b635e6d1894 100644 --- a/fda-authentication-service/rest-service/src/test/resources/application.properties +++ b/fda-authentication-service/rest-service/src/test/resources/application.properties @@ -9,7 +9,7 @@ spring.cloud.config.discovery.enabled = false spring.cloud.config.enabled = false # disable datasource -spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=FALSE;INIT=CREATE SCHEMA IF NOT EXISTS FDA +spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=FALSE;INIT=runscript from './src/test/resources/pre-init.sql' spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password diff --git a/fda-authentication-service/rest-service/src/test/resources/post-init.sql b/fda-authentication-service/rest-service/src/test/resources/post-init.sql new file mode 100644 index 0000000000000000000000000000000000000000..4e18b3e6e26aed4aac42a399f417d2cdf9afd5be --- /dev/null +++ b/fda-authentication-service/rest-service/src/test/resources/post-init.sql @@ -0,0 +1,8 @@ +-- Modified for H2 +-- Assume id=1 is invalid +-- Assume id=2 is still valid token +CREATE VIEW IF NOT EXISTS mdb_valid_tokens AS +( +SELECT `id`, `token_hash`, `creator`, `created`, `expires`, `last_used` +FROM (SELECT `id`, `token_hash`, `creator`, `created`, `expires`, `last_used` FROM FDA.`mdb_tokens`) +WHERE `id` != 1 GROUP BY `id`); \ No newline at end of file diff --git a/fda-authentication-service/rest-service/src/test/resources/pre-init.sql b/fda-authentication-service/rest-service/src/test/resources/pre-init.sql new file mode 100644 index 0000000000000000000000000000000000000000..173e6d5b1c05fe3788b0b5c964caed733e6c41f0 --- /dev/null +++ b/fda-authentication-service/rest-service/src/test/resources/pre-init.sql @@ -0,0 +1 @@ +CREATE SCHEMA IF NOT EXISTS FDA; \ No newline at end of file diff --git a/fda-authentication-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java b/fda-authentication-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java index eb8d9548da45e84a3a25b9d55a759387bc9d8fdd..dac68f16eebc970088c4e42f1096f9a13cbb3316 100644 --- a/fda-authentication-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java +++ b/fda-authentication-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java @@ -1,5 +1,6 @@ package at.tuwien.auth; +import at.tuwien.service.TokenService; import at.tuwien.service.impl.UserDetailsServiceImpl; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -20,10 +21,12 @@ import java.io.IOException; public class AuthTokenFilter extends OncePerRequestFilter { private final JwtUtils jwtUtils; + private final TokenService tokenService; private final UserDetailsServiceImpl userDetailsService; - public AuthTokenFilter(JwtUtils jwtUtils, UserDetailsServiceImpl userDetailsService) { + public AuthTokenFilter(JwtUtils jwtUtils, TokenService tokenService, UserDetailsServiceImpl userDetailsService) { this.jwtUtils = jwtUtils; + this.tokenService = tokenService; this.userDetailsService = userDetailsService; } @@ -32,6 +35,7 @@ public class AuthTokenFilter extends OncePerRequestFilter { throws ServletException, IOException { final String jwt = parseJwt(request); if (jwt != null && jwtUtils.validateJwtToken(jwt)) { + tokenService.check(jwt); final String username = jwtUtils.getUserNameFromJwtToken(jwt); final UserDetails userDetails; try { diff --git a/fda-authentication-service/services/src/main/java/at/tuwien/auth/JwtUtils.java b/fda-authentication-service/services/src/main/java/at/tuwien/auth/JwtUtils.java index 36fcb3b5e2d76be64aa9242cdcba77b638431124..12e93c0eb1632ba18ed9606747d534033c5baacf 100644 --- a/fda-authentication-service/services/src/main/java/at/tuwien/auth/JwtUtils.java +++ b/fda-authentication-service/services/src/main/java/at/tuwien/auth/JwtUtils.java @@ -5,6 +5,7 @@ import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.interfaces.DecodedJWT; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.digest.DigestUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -40,6 +41,10 @@ public class JwtUtils { .getSubject(); } + public static String toHash(String token) { + return DigestUtils.sha256Hex(token); + } + public boolean validateJwtToken(String authToken) { try { final DecodedJWT jwt = JWT.decode(authToken); diff --git a/fda-authentication-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/fda-authentication-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java index ac11ba38db64d0aeb6a9a3944e53dd850f53365d..56851d0743df3b0a4fd6c15b6e12f28e89a53c42 100644 --- a/fda-authentication-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java +++ b/fda-authentication-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java @@ -2,6 +2,7 @@ package at.tuwien.config; import at.tuwien.auth.AuthTokenFilter; import at.tuwien.auth.JwtUtils; +import at.tuwien.service.TokenService; import at.tuwien.service.impl.UserDetailsServiceImpl; import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.security.SecurityScheme; @@ -35,20 +36,22 @@ import javax.servlet.http.HttpServletResponse; public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private final JwtUtils jwtUtils; + private final TokenService tokenService; private final SecurityConfig securityConfig; private final UserDetailsServiceImpl userDetailsService; @Autowired - public WebSecurityConfig(JwtUtils jwtUtils, SecurityConfig securityConfig, + public WebSecurityConfig(JwtUtils jwtUtils, TokenService tokenService, SecurityConfig securityConfig, UserDetailsServiceImpl userDetailsService) { this.jwtUtils = jwtUtils; + this.tokenService = tokenService; this.securityConfig = securityConfig; this.userDetailsService = userDetailsService; } @Bean public AuthTokenFilter authTokenFilter() { - return new AuthTokenFilter(jwtUtils, userDetailsService); + return new AuthTokenFilter(jwtUtils, tokenService, userDetailsService); } @Override diff --git a/fda-authentication-service/services/src/main/java/at/tuwien/repositories/TokenRepository.java b/fda-authentication-service/services/src/main/java/at/tuwien/repositories/TokenRepository.java index 998fdcbd14dabadd41dfc1862534adc4e4291860..9e78e638782ac5e0f602383debf8f1cd3a7057b0 100644 --- a/fda-authentication-service/services/src/main/java/at/tuwien/repositories/TokenRepository.java +++ b/fda-authentication-service/services/src/main/java/at/tuwien/repositories/TokenRepository.java @@ -17,4 +17,7 @@ public interface TokenRepository extends JpaRepository<Token, Long> { Optional<Token> findByTokenHash(String tokenHash); + + Optional<Token> findByValidTokenHash(@Param("hash") String tokenHash); + } diff --git a/fda-authentication-service/services/src/main/java/at/tuwien/service/TokenService.java b/fda-authentication-service/services/src/main/java/at/tuwien/service/TokenService.java index 26d5e4f33d8a2d4b4ae317f9a7527704c19e1982..caeeb8d41f9532c40b167a6758c8f5e542355d12 100644 --- a/fda-authentication-service/services/src/main/java/at/tuwien/service/TokenService.java +++ b/fda-authentication-service/services/src/main/java/at/tuwien/service/TokenService.java @@ -5,7 +5,9 @@ import at.tuwien.exception.TokenNotEligableException; import at.tuwien.exception.TokenNotFoundException; import at.tuwien.exception.UserNotFoundException; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import javax.servlet.ServletException; import java.security.Principal; import java.util.List; @@ -40,6 +42,15 @@ public interface TokenService { */ Token findOne(String tokenHash) throws TokenNotFoundException; + /** + * Finds a token by id. + * + * @param id The token id. + * @return The token, if successful. + * @throws TokenNotFoundException The token was not found in the metadata database. + */ + Token findOne(Long id) throws TokenNotFoundException; + /** * Deletes a developer token in the metadata database by hash and user principal. * @@ -49,4 +60,11 @@ public interface TokenService { * @throws UserNotFoundException The user does not exist in the metadata database. */ void delete(String tokenHash, Principal principal) throws TokenNotFoundException, UserNotFoundException; + + /** + * Checks if the developer token has not been marked as deleted + * + * @param jwt The token + */ + void check(String jwt) throws ServletException; } diff --git a/fda-authentication-service/services/src/main/java/at/tuwien/service/impl/TokenServiceImpl.java b/fda-authentication-service/services/src/main/java/at/tuwien/service/impl/TokenServiceImpl.java index e7f34b4cf7cfaf058b385ffb03a44c4feffe33cb..283109c46703905607a4cbedd786f9dbaec1442a 100644 --- a/fda-authentication-service/services/src/main/java/at/tuwien/service/impl/TokenServiceImpl.java +++ b/fda-authentication-service/services/src/main/java/at/tuwien/service/impl/TokenServiceImpl.java @@ -15,6 +15,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import javax.servlet.ServletException; import java.security.Principal; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -80,6 +81,17 @@ public class TokenServiceImpl implements TokenService { return optional.get(); } + @Override + @Transactional(readOnly = true) + public Token findOne(Long id) throws TokenNotFoundException { + final Optional<Token> optional = tokenRepository.findById(id); + if (optional.isEmpty()) { + log.error("Failed to find token with id {}", id); + throw new TokenNotFoundException("Failed to find token"); + } + return optional.get(); + } + @Override @Transactional public void delete(String tokenHash, Principal principal) throws TokenNotFoundException, UserNotFoundException { @@ -94,4 +106,18 @@ public class TokenServiceImpl implements TokenService { log.debug("deleted token {}", token); } + @Override + @Transactional + public void check(String jwt) throws ServletException { + final String tokenHash = JwtUtils.toHash(jwt); + final Optional<Token> optional = tokenRepository.findByValidTokenHash(tokenHash); + if (optional.isEmpty()) { + log.error("Token with hash {} is marked as revoked", tokenHash); + throw new ServletException("Token with hash " + tokenHash + " is marked as revoked"); + } + final Token token = optional.get(); + token.setLastUsed(Instant.now()); + tokenRepository.save(token); + } + } diff --git a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/Token.java b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/Token.java index d1ae37cfab91fd59117b12602a5ceece3f167392..edf7ae144c28a4cab9b87e25067af12800bf0019 100644 --- a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/Token.java +++ b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/Token.java @@ -3,7 +3,6 @@ package at.tuwien.entities.user; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import org.hibernate.annotations.GenericGenerator; -import org.hibernate.annotations.SQLDelete; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -19,6 +18,11 @@ import java.time.Instant; @EntityListeners(AuditingEntityListener.class) @EqualsAndHashCode(onlyExplicitlyIncluded = true) @Table(name = "mdb_tokens") +@NamedNativeQueries({ + @NamedNativeQuery(name = "Token.findByValidTokenHash", + query = "SELECT * FROM `mdb_valid_tokens` WHERE `token_hash` = :hash", + resultClass = Token.class) +}) public class Token { @Id @@ -46,7 +50,7 @@ public class Token { @Column(nullable = false, updatable = false) private Instant expires; - @Column(nullable = false, updatable = false) + @Column private Instant lastUsed; } diff --git a/fda-metadata-db/setup-schema.sql b/fda-metadata-db/setup-schema.sql index b58d7cde3db2e599c719bd5fbdfe98606970c9a6..e48b9dbd1737808f2399f866e6dc309f8fa9abd1 100644 --- a/fda-metadata-db/setup-schema.sql +++ b/fda-metadata-db/setup-schema.sql @@ -23,7 +23,7 @@ CREATE TABLE IF NOT EXISTS mdb_users UNIQUE (username), UNIQUE (Main_Email), UNIQUE (OID) -); +) WITH SYSTEM VERSIONING; CREATE TABLE mdb_images ( @@ -41,7 +41,7 @@ CREATE TABLE mdb_images last_modified timestamp, PRIMARY KEY (id), UNIQUE (repository, tag) -); +) WITH SYSTEM VERSIONING; CREATE TABLE mdb_time_secrets ( @@ -53,7 +53,7 @@ CREATE TABLE mdb_time_secrets valid_to timestamp NOT NULL, PRIMARY KEY (id), FOREIGN KEY (uid) REFERENCES mdb_users (UserID) -); +) WITH SYSTEM VERSIONING; CREATE TABLE mdb_tokens ( @@ -65,7 +65,7 @@ CREATE TABLE mdb_tokens last_used timestamp, PRIMARY KEY (id), FOREIGN KEY (creator) REFERENCES mdb_users (UserID) -); +) WITH SYSTEM VERSIONING; CREATE TABLE mdb_images_date ( @@ -79,7 +79,7 @@ CREATE TABLE mdb_images_date PRIMARY KEY (id), FOREIGN KEY (iid) REFERENCES mdb_images (id), UNIQUE (database_format, unix_format, example) -); +) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS mdb_containers ( @@ -96,7 +96,7 @@ CREATE TABLE IF NOT EXISTS mdb_containers PRIMARY KEY (id), FOREIGN KEY (created_by) REFERENCES mdb_users (UserID), FOREIGN KEY (image_id) REFERENCES mdb_images (id) -); +) WITH SYSTEM VERSIONING; CREATE TABLE mdb_images_environment_item ( @@ -109,7 +109,7 @@ CREATE TABLE mdb_images_environment_item last_modified timestamp, PRIMARY KEY (id, iid), FOREIGN KEY (iid) REFERENCES mdb_images (id) -); +) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS mdb_data ( @@ -120,7 +120,7 @@ CREATE TABLE IF NOT EXISTS mdb_data Version TEXT, Seperator TEXT, PRIMARY KEY (ID) -); +) WITH SYSTEM VERSIONING CREATE TABLE IF NOT EXISTS mdb_user_roles ( @@ -131,7 +131,7 @@ CREATE TABLE IF NOT EXISTS mdb_user_roles PRIMARY KEY (uid), FOREIGN KEY (uid) REFERENCES mdb_users (UserID), UNIQUE (uid, role) -); +) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS mdb_licenses ( @@ -139,7 +139,7 @@ CREATE TABLE IF NOT EXISTS mdb_licenses uri TEXT NOT NULL, PRIMARY KEY (identifier), UNIQUE (uri) -); +) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS mdb_databases ( @@ -158,14 +158,14 @@ CREATE TABLE IF NOT EXISTS mdb_databases FOREIGN KEY (created_by) REFERENCES mdb_users (UserID), FOREIGN KEY (contact_person) REFERENCES mdb_users (UserID), FOREIGN KEY (id) REFERENCES mdb_containers (id) /* currently we only support one-to-one */ -); +) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS mdb_databases_subjects ( dbid BIGINT NOT NULL, subjects character varying(255) NOT NULL, PRIMARY KEY (dbid, subjects) -); +) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS mdb_tables ( @@ -190,7 +190,7 @@ CREATE TABLE IF NOT EXISTS mdb_tables PRIMARY KEY (ID, tDBID), FOREIGN KEY (created_by) REFERENCES mdb_users (UserID), FOREIGN KEY (tDBID) REFERENCES mdb_databases (id) -); +) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS mdb_columns ( @@ -215,7 +215,7 @@ CREATE TABLE IF NOT EXISTS mdb_columns FOREIGN KEY (cDBID, tID) REFERENCES mdb_tables (tDBID, ID), FOREIGN KEY (created_by) REFERENCES mdb_users (UserID), PRIMARY KEY (ID, cDBID, tID) -); +) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS mdb_columns_enums ( @@ -228,7 +228,7 @@ CREATE TABLE IF NOT EXISTS mdb_columns_enums last_modified timestamp, FOREIGN KEY (eDBID, tID, cID) REFERENCES mdb_columns (cDBID, tID, ID), PRIMARY KEY (ID, eDBID, tID, cID) -); +) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS mdb_columns_nom ( @@ -240,7 +240,7 @@ CREATE TABLE IF NOT EXISTS mdb_columns_nom created timestamp NOT NULL DEFAULT NOW(), FOREIGN KEY (cDBID, tID, cID) REFERENCES mdb_columns (cDBID, tID, ID), PRIMARY KEY (cDBID, tID, cID) -); +) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS mdb_columns_num ( @@ -258,7 +258,7 @@ CREATE TABLE IF NOT EXISTS mdb_columns_num created timestamp NOT NULL DEFAULT NOW(), FOREIGN KEY (cDBID, tID, cID) REFERENCES mdb_columns (cDBID, tID, ID), PRIMARY KEY (cDBID, tID, cID) -); +) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS mdb_columns_cat ( @@ -271,7 +271,7 @@ CREATE TABLE IF NOT EXISTS mdb_columns_cat created timestamp NOT NULL DEFAULT NOW(), FOREIGN KEY (cDBID, tID, cID) REFERENCES mdb_columns (cDBID, tID, ID), PRIMARY KEY (cDBID, tID, cID) -); +) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS mdb_concepts ( @@ -282,7 +282,7 @@ CREATE TABLE IF NOT EXISTS mdb_concepts created_by bigint, FOREIGN KEY (created_by) REFERENCES mdb_users (UserID), PRIMARY KEY (id) -); +) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS mdb_columns_concepts ( @@ -295,7 +295,7 @@ CREATE TABLE IF NOT EXISTS mdb_columns_concepts FOREIGN KEY (cDBID, tID, cID) REFERENCES mdb_columns (cDBID, tID, ID), FOREIGN KEY (concept_id) REFERENCES mdb_concepts (id), PRIMARY KEY (cDBID, tID, cID) -); +) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS mdb_view ( @@ -315,7 +315,7 @@ CREATE TABLE IF NOT EXISTS mdb_view FOREIGN KEY (created_by) REFERENCES mdb_users (UserID), FOREIGN KEY (vdbid) REFERENCES mdb_databases (id), PRIMARY KEY (id, vdbid) -); +) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS mdb_identifiers ( @@ -348,7 +348,7 @@ CREATE TABLE IF NOT EXISTS mdb_identifiers FOREIGN KEY (dbid) REFERENCES mdb_databases (id), FOREIGN KEY (created_by) REFERENCES mdb_users (UserID), UNIQUE (cid, dbid, qid) -); +) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS mdb_related_identifiers ( @@ -363,7 +363,7 @@ CREATE TABLE IF NOT EXISTS mdb_related_identifiers PRIMARY KEY (id, iid), /* must be a single id from persistent identifier concept */ FOREIGN KEY (iid) REFERENCES mdb_identifiers (id), FOREIGN KEY (created_by) REFERENCES mdb_users (UserID) -); +) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS mdb_creators ( @@ -378,7 +378,7 @@ CREATE TABLE IF NOT EXISTS mdb_creators FOREIGN KEY (created_by) REFERENCES mdb_users (UserID), PRIMARY KEY (id, pid), FOREIGN KEY (pid) REFERENCES mdb_identifiers (id) -); +) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS mdb_feed ( @@ -389,7 +389,7 @@ CREATE TABLE IF NOT EXISTS mdb_feed created timestamp NOT NULL DEFAULT NOW(), FOREIGN KEY (fDBID, fID) REFERENCES mdb_tables (tDBID, ID), PRIMARY KEY (fDBID, fID, fUserId, fDataID) -); +) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS mdb_update ( @@ -397,7 +397,7 @@ CREATE TABLE IF NOT EXISTS mdb_update uDBID bigint REFERENCES mdb_databases (id), created timestamp NOT NULL DEFAULT NOW(), PRIMARY KEY (uUserID, uDBID) -); +) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS mdb_access ( @@ -407,7 +407,7 @@ CREATE TABLE IF NOT EXISTS mdb_access download BOOLEAN, created timestamp NOT NULL DEFAULT NOW(), PRIMARY KEY (aUserID, aDBID) -); +) WITH SYSTEM VERSIONING CREATE TABLE IF NOT EXISTS mdb_have_access ( @@ -416,7 +416,7 @@ CREATE TABLE IF NOT EXISTS mdb_have_access hType ENUM ('R', 'W'), created timestamp NOT NULL DEFAULT NOW(), PRIMARY KEY (hUserID, hDBID) -); +) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS mdb_owns ( @@ -424,7 +424,18 @@ CREATE TABLE IF NOT EXISTS mdb_owns oDBID bigint REFERENCES mdb_databases (ID), created timestamp NOT NULL DEFAULT NOW(), PRIMARY KEY (oUserID, oDBID) -); +) WITH SYSTEM VERSIONING; + +CREATE VIEW IF NOT EXISTS mdb_valid_tokens AS +( +SELECT `id`, `token_hash`, `creator`, `created`, `expires`, `last_used` +FROM (SELECT `id`, `token_hash`, `creator`, `created`, `expires`, `last_used` + FROM `mdb_tokens` FOR SYSTEM_TIME ALL) as t +WHERE NOT EXISTS(SELECT `token_hash` + FROM mdb_tokens AS tt + WHERE ROW_END > NOW() + AND tt.`token_hash` = t.`token_hash`) +GROUP BY `id`); COMMIT; diff --git a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/AbstractEndpoint.java b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/AbstractEndpoint.java index 8da23987b629668dc2cd1fa679775771c270bf7f..af0232f142d04469d981aedf100e7aa8b1e4ebde 100644 --- a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/AbstractEndpoint.java +++ b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/AbstractEndpoint.java @@ -1,21 +1,25 @@ package at.tuwien.endpoint; import at.tuwien.SortType; +import at.tuwien.api.database.query.ExecuteStatementDto; import at.tuwien.entities.database.Database; import at.tuwien.entities.database.table.Table; import at.tuwien.entities.identifier.Identifier; -import at.tuwien.exception.DatabaseNotFoundException; -import at.tuwien.exception.IdentifierNotFoundException; -import at.tuwien.exception.PaginationException; -import at.tuwien.exception.SortException; +import at.tuwien.exception.*; import at.tuwien.service.DatabaseService; import at.tuwien.service.IdentifierService; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; import java.security.Principal; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static at.tuwien.entities.identifier.VisibilityType.EVERYONE; @@ -99,6 +103,25 @@ public abstract class AbstractEndpoint { } } + protected void validateForbiddenStatements(ExecuteStatementDto data) throws QueryMalformedException, + QueryStoreException { + final StringBuilder regex = new StringBuilder("["); + try { + FileUtils.readLines(new File("src/main/resources/forbidden.txt"), Charset.defaultCharset()) + .forEach(regex::append); + } catch (IOException e) { + log.error("Failed to load forbidden keywords list, reason {}", e.getMessage()); + throw new QueryStoreException("Failed to load forbidden keywords list", e); + } + final Pattern pattern = Pattern.compile(regex + "]"); + final Matcher matcher = pattern.matcher(data.getStatement()); + final boolean found = matcher.find(); + if (found) { + log.error("Query contains blacklisted character"); + throw new QueryMalformedException("Query contains blacklisted character"); + } + } + protected Boolean hasQueuePermission(Long containerId, Long databaseId, Long tableId, String permissionCode, Principal principal) { log.trace("validate queue permission, containerId={}, databaseId={}, tableId={}, permissionCode={}, principal={}", diff --git a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java index 0699f5b758213fc5e8cc60c280eed63fe16bae81..3abdccb05c4079688a4baa84a60dd778f1156d0d 100644 --- a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java +++ b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java @@ -20,6 +20,8 @@ import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import javax.validation.constraints.NotNull; import java.security.Principal; +import java.util.regex.Matcher; +import java.util.regex.Pattern; @Log4j2 @RestController @@ -63,6 +65,7 @@ public class QueryEndpoint extends AbstractEndpoint { log.error("Failed to execute query: is empty"); throw new QueryMalformedException("Failed to execute query"); } + validateForbiddenStatements(data); validateDataParams(page, size, sortDirection, sortColumn); /* execute */ final QueryResultDto result = queryService.execute(containerId, databaseId, data, QueryTypeDto.QUERY, diff --git a/fda-query-service/rest-service/src/main/resources/forbidden.txt b/fda-query-service/rest-service/src/main/resources/forbidden.txt new file mode 100644 index 0000000000000000000000000000000000000000..89bdcae71069ee0e80035dbc2a0143d570ab253d --- /dev/null +++ b/fda-query-service/rest-service/src/main/resources/forbidden.txt @@ -0,0 +1 @@ + \* \ No newline at end of file diff --git a/fda-query-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java b/fda-query-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java index 2045c6a86f5c6833138891efd70514f512edc902..4c268cc350d5a2f158ee15b446a36acf8308cb5d 100644 --- a/fda-query-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java +++ b/fda-query-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java @@ -2,6 +2,8 @@ package at.tuwien; import at.tuwien.api.database.query.QueryBriefDto; import at.tuwien.api.database.query.QueryDto; +import at.tuwien.api.database.query.QueryResultDto; +import at.tuwien.api.user.UserDetailsDto; import at.tuwien.api.user.UserDto; import at.tuwien.entities.container.image.ContainerImageDate; import at.tuwien.entities.database.table.columns.concepts.Concept; @@ -16,11 +18,17 @@ import at.tuwien.entities.database.Database; import at.tuwien.entities.database.table.Table; import at.tuwien.entities.database.table.columns.TableColumn; import at.tuwien.entities.database.table.columns.TableColumnType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.test.context.TestPropertySource; +import java.security.Principal; import java.time.Instant; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static java.time.temporal.ChronoUnit.*; @@ -30,27 +38,41 @@ public abstract class BaseUnitTest { public final static long USER_1_ID = 1; public final static String USER_1_USERNAME = "junit"; public final static String USER_1_EMAIL = "junit@example.com"; + public final static String USER_1_PASSWORD = "password"; + public final static Instant USER_1_CREATED = Instant.now().minus(1, HOURS); + public final static User USER_1 = User.builder() .id(USER_1_ID) .username(USER_1_USERNAME) .email(USER_1_EMAIL) .emailVerified(true) .themeDark(false) - .password("password") + .password(USER_1_PASSWORD) .roles(Collections.singletonList(RoleType.ROLE_RESEARCHER)) .created(USER_1_CREATED) .lastModified(USER_1_CREATED) .build(); + public final static UserDto USER_1_DTO = UserDto.builder() .id(USER_1_ID) .username(USER_1_USERNAME) .email(USER_1_EMAIL) .emailVerified(true) .themeDark(false) - .password("password") + .password(USER_1_PASSWORD) .build(); + public final static UserDetails USER_1_DETAILS = UserDetailsDto.builder() + .username(USER_1_USERNAME) + .email(USER_1_EMAIL) + .password(USER_1_PASSWORD) + .authorities(List.of(new SimpleGrantedAuthority("ROLE_RESEARCHER"))) + .build(); + + public final static Principal USER_1_PRINCIPAL = new UsernamePasswordAuthenticationToken(USER_1_DETAILS, + USER_1_PASSWORD, USER_1_DETAILS.getAuthorities()); + public final static String DATABASE_NET = "fda-userdb"; public final static String BROKER_IMAGE = "fda-broker-service:latest"; @@ -1893,4 +1915,24 @@ public abstract class BaseUnitTest { .exchange(DATABASE_3_EXCHANGE) .build(); + public final static Long QUERY_1_RESULT_ID = 1L; + public final static Long QUERY_1_RESULT_NUMBER = 2L; + public final static List<Map<String, Object>> QUERY_1_RESULT_RESULT = List.of( + new HashMap<>() {{ + put("location", "Albury"); + put("lat", -36.0653583); + put("lng", 146.9112214); + }}, new HashMap<>() {{ + put("location", "Sydney"); + put("lat", -33.847927); + put("lng", 150.6517942); + }}); + + public final static QueryResultDto QUERY_1_RESULT_DTO = QueryResultDto.builder() + .id(QUERY_1_RESULT_ID) + .resultNumber(QUERY_1_RESULT_NUMBER) + .result(QUERY_1_RESULT_RESULT) + .build(); + + } diff --git a/fda-query-service/rest-service/src/test/java/at/tuwien/endpoint/QueryEndpointUnitTest.java b/fda-query-service/rest-service/src/test/java/at/tuwien/endpoint/QueryEndpointUnitTest.java index 7d53a289675c58b64d13628c7b8aca173d1920ad..86d07a94fa9b329b844cb7f32906f9d12bb51443 100644 --- a/fda-query-service/rest-service/src/test/java/at/tuwien/endpoint/QueryEndpointUnitTest.java +++ b/fda-query-service/rest-service/src/test/java/at/tuwien/endpoint/QueryEndpointUnitTest.java @@ -1,15 +1,34 @@ package at.tuwien.endpoint; import at.tuwien.BaseUnitTest; +import at.tuwien.SortType; +import at.tuwien.api.database.query.ExecuteStatementDto; +import at.tuwien.api.database.query.QueryResultDto; +import at.tuwien.api.database.query.QueryTypeDto; import at.tuwien.config.ReadyConfig; +import at.tuwien.exception.*; import at.tuwien.listener.impl.RabbitMqListenerImpl; +import at.tuwien.repository.jpa.ContainerRepository; +import at.tuwien.repository.jpa.DatabaseRepository; +import at.tuwien.repository.jpa.ImageRepository; +import at.tuwien.service.QueryService; +import at.tuwien.service.StoreService; import com.rabbitmq.client.Channel; import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.test.context.junit.jupiter.SpringExtension; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + @Log4j2 @SpringBootTest @ExtendWith(SpringExtension.class) @@ -24,4 +43,75 @@ public class QueryEndpointUnitTest extends BaseUnitTest { @MockBean private RabbitMqListenerImpl rabbitMqListener; + @MockBean + private ImageRepository imageRepository; + + @MockBean + private ContainerRepository containerRepository; + + @MockBean + private DatabaseRepository databaseRepository; + + @MockBean + private QueryService queryService; + + @MockBean + private StoreService storeService; + + @Autowired + private QueryEndpoint queryEndpoint; + + @Test + public void execute_forbiddenKeyword_fails() throws UserNotFoundException, QueryStoreException, + TableMalformedException, DatabaseConnectionException, QueryMalformedException, ColumnParseException, + DatabaseNotFoundException, ImageNotSupportedException, ContainerNotFoundException { + final ExecuteStatementDto request = ExecuteStatementDto.builder() + .statement("SELECT w.* FROM `weather_aus` w") + .build(); + final Long page = 0L; + final Long size = 2L; + final SortType sortDirection = SortType.ASC; + final String sortColumn = "location"; + + /* mock */ + when(databaseRepository.findByContainerIdAndDatabaseId(CONTAINER_1_ID, DATABASE_1_ID)) + .thenReturn(Optional.of(DATABASE_1)); + when(queryService.execute(CONTAINER_1_ID, DATABASE_1_ID, request, QueryTypeDto.QUERY, + USER_1_PRINCIPAL, page, size, sortDirection, sortColumn)) + .thenReturn(QUERY_1_RESULT_DTO); + + /* test */ + assertThrows(QueryMalformedException.class, () -> { + queryEndpoint.execute(CONTAINER_1_ID, DATABASE_1_ID, request, page, size, USER_1_PRINCIPAL, sortDirection, + sortColumn); + }); + } + + @Test + public void execute_forbiddenKeyword2_fails() throws UserNotFoundException, QueryStoreException, + TableMalformedException, DatabaseConnectionException, QueryMalformedException, ColumnParseException, + DatabaseNotFoundException, ImageNotSupportedException, ContainerNotFoundException { + final ExecuteStatementDto request = ExecuteStatementDto.builder() + .statement("SELECT * FROM `weather_aus` w") + .build(); + final Long page = 0L; + final Long size = 2L; + final SortType sortDirection = SortType.ASC; + final String sortColumn = "location"; + + /* mock */ + when(databaseRepository.findByContainerIdAndDatabaseId(CONTAINER_1_ID, DATABASE_1_ID)) + .thenReturn(Optional.of(DATABASE_1)); + when(queryService.execute(CONTAINER_1_ID, DATABASE_1_ID, request, QueryTypeDto.QUERY, + USER_1_PRINCIPAL, page, size, sortDirection, sortColumn)) + .thenReturn(QUERY_1_RESULT_DTO); + + /* test */ + assertThrows(QueryMalformedException.class, () -> { + queryEndpoint.execute(CONTAINER_1_ID, DATABASE_1_ID, request, page, size, USER_1_PRINCIPAL, sortDirection, + sortColumn); + }); + } + + } diff --git a/fda-ui/layouts/default.vue b/fda-ui/layouts/default.vue index 63c821f9eed411adbe629b3af998f1f07b0c4cd0..919a3359c3cc76f21e02575667be36cb98e5ac7c 100644 --- a/fda-ui/layouts/default.vue +++ b/fda-ui/layouts/default.vue @@ -263,8 +263,8 @@ export default { } }, login () { - let redirect = ![undefined ,'/', '/login'].includes(this.$router.currentRoute.path) - this.$router.push({ path: '/login', query: redirect ? { redirect: this.$router.currentRoute.path } : {}}) + const redirect = ![undefined, '/', '/login'].includes(this.$router.currentRoute.path) + this.$router.push({ path: '/login', query: redirect ? { redirect: this.$router.currentRoute.path } : {} }) }, navigate (item) { this.$router.push(this.metadata(item).link) diff --git a/fda-ui/nuxt.config.js b/fda-ui/nuxt.config.js index 08afe5b2e157211bc6a6352dd5343badea42db39..902ab4c355e844468c25af4a737178b917ab9560 100644 --- a/fda-ui/nuxt.config.js +++ b/fda-ui/nuxt.config.js @@ -77,7 +77,8 @@ export default { sharedFilesystem: process.env.SHARED_FILESYSTEM || '/tmp', version: process.env.VERSION || 'latest', logo: process.env.LOGO || '/logo.png', - mailVerify: process.env.MAIL_VERIFY || false + mailVerify: process.env.MAIL_VERIFY || false, + tokenMax: process.env.TOKEN_MAX || 5 }, proxy: { diff --git a/fda-ui/pages/login.vue b/fda-ui/pages/login.vue index b9b82425b216eb84ac34d9bedfbdef1765e72778..caf8bf208eb8768656cd7a62bccc8f5ea3af974d 100644 --- a/fda-ui/pages/login.vue +++ b/fda-ui/pages/login.vue @@ -96,7 +96,7 @@ export default { delete user.token this.$store.commit('SET_USER', user) this.$toast.success('Welcome back!') - this.$router.push(this.$route.query.redirect ? this.$route.query.redirect : '/container') + this.$router.push(this.$route.query.redirect ? this.$route.query.redirect : '/container') } catch (err) { if (err.response !== undefined && err.response.status !== undefined) { if (err.response.status === 418) { diff --git a/fda-ui/pages/user/developer.vue b/fda-ui/pages/user/developer.vue index bdc5f38bfe235be6489c3348793df1be5390645f..9dc9bf35fade6bf5784ef4116e9b62a69c0646e8 100644 --- a/fda-ui/pages/user/developer.vue +++ b/fda-ui/pages/user/developer.vue @@ -9,9 +9,10 @@ <v-card-text> <v-list-item v-for="(item, i) in tokens" :key="i" three-line> <v-list-item-content> - <v-list-item-title>sha256:{{ item.token_hash }}</v-list-item-title> - <v-list-item-subtitle v-if="!item.token"> - Created on {{ format(item.created) }}, Valid until: {{ format(item.expires) }}</v-list-item-subtitle> + <v-list-item-title :class="tokenClass(item)">sha256:{{ item.token_hash }}</v-list-item-title> + <v-list-item-subtitle v-if="!item.token" :class="tokenClass(item)"> + Last used: <span v-if="item.last_used">{{ format(item.last_used) }}</span><span v-if="!item.last_used">Never</span> — valid until: {{ format(item.expires) }} + </v-list-item-subtitle> <v-list-item-subtitle v-if="item.token"> <v-text-field v-model="item.token" @@ -23,12 +24,12 @@ @click:append-outer="copy(item)" /> </v-list-item-subtitle> <v-list-item-subtitle v-if="!item.token"> - <a @click="revokeToken(item.token_hash)">Revoke Token</a> + <a @click="revokeToken(item.id)">Revoke Token</a> </v-list-item-subtitle> </v-list-item-content> </v-list-item> - <v-btn class="mt-4" x-small @click="mintToken"> - Mint Token + <v-btn :disabled="tokens.length >= tokenMax" class="mt-4" color="secondary" small @click="mintToken"> + Create Token </v-btn> </v-card-text> </v-card> @@ -59,6 +60,9 @@ export default { return { headers: { Authorization: `Bearer ${this.token}` } } + }, + tokenMax () { + return this.$config.tokenMax } }, mounted () { @@ -74,6 +78,9 @@ export default { format (timestamp) { return formatTimestamp(timestamp) }, + tokenClass (token) { + return token.last_used ? '' : 'token-not_used' + }, async loadTokens () { this.loading = true try { @@ -102,10 +109,10 @@ export default { } this.loading = false }, - async revokeToken (hash) { + async revokeToken (id) { this.loading = true try { - await this.$axios.delete(`/api/user/token/${hash}`, this.config) + await this.$axios.delete(`/api/user/token/${id}`, this.config) await this.loadTokens() } catch (err) { this.$toast.error('Could not delete token') @@ -115,3 +122,8 @@ export default { } } </script> +<style> +.token-not_used { + opacity: 0.4; +} +</style>