From 4e07a6bcf7a95880ebe0c5322fdff5ef1178fa45 Mon Sep 17 00:00:00 2001 From: Fabian Engelniederhammer Date: Mon, 17 Apr 2023 11:26:43 +0200 Subject: [PATCH] feat: get access key from request and read valid access keys from file issue: #218 --- .../genspectrum/lapis/LapisSpringConfig.kt | 18 +- .../auth/DataOpennessAuthorizationFilter.kt | 110 +++++++++--- .../genspectrum/lapis/config/AccessKeys.kt | 23 +++ .../lapis/config/DatabaseConfig.kt | 2 +- .../lapis/controller/LapisController.kt | 16 +- .../util/CachedBodyHttpServletRequest.kt | 46 +++++ .../lapis/util/YamlObjectMapper.kt | 11 ++ .../lapis/auth/GisaidAuthorizationTest.kt | 164 +++++++++++++++++- .../lapis/config/AccessKeysReaderTest.kt | 36 ++++ .../resources/application-test.properties | 1 + ...plication-testWithoutAccessKeys.properties | 4 + .../config/gisaidDatabaseConfig.yaml | 9 +- .../test/resources/config/testAccessKeys.yaml | 2 + .../testDatabaseConfigWithoutFeatures.yaml | 1 + 14 files changed, 395 insertions(+), 48 deletions(-) create mode 100644 lapis2/src/main/kotlin/org/genspectrum/lapis/config/AccessKeys.kt create mode 100644 lapis2/src/main/kotlin/org/genspectrum/lapis/util/CachedBodyHttpServletRequest.kt create mode 100644 lapis2/src/main/kotlin/org/genspectrum/lapis/util/YamlObjectMapper.kt create mode 100644 lapis2/src/test/kotlin/org/genspectrum/lapis/config/AccessKeysReaderTest.kt create mode 100644 lapis2/src/test/resources/application-testWithoutAccessKeys.properties create mode 100644 lapis2/src/test/resources/config/testAccessKeys.yaml diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/LapisSpringConfig.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/LapisSpringConfig.kt index 34469327..4da0888a 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/LapisSpringConfig.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/LapisSpringConfig.kt @@ -1,17 +1,15 @@ package org.genspectrum.lapis -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import com.fasterxml.jackson.module.kotlin.readValue -import com.fasterxml.jackson.module.kotlin.registerKotlinModule import mu.KotlinLogging -import org.genspectrum.lapis.auth.DataOpennessAuthorizationFilter +import org.genspectrum.lapis.auth.DataOpennessAuthorizationFilterFactory import org.genspectrum.lapis.config.DatabaseConfig import org.genspectrum.lapis.config.SequenceFilterFields import org.genspectrum.lapis.logging.RequestContext import org.genspectrum.lapis.logging.RequestContextLogger import org.genspectrum.lapis.logging.StatisticsLogObjectMapper import org.genspectrum.lapis.util.TimeFactory +import org.genspectrum.lapis.util.YamlObjectMapper import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -24,8 +22,11 @@ class LapisSpringConfig { fun openAPI(sequenceFilterFields: SequenceFilterFields) = buildOpenApiSchema(sequenceFilterFields) @Bean - fun databaseConfig(@Value("\${lapis.databaseConfig.path}") configPath: String): DatabaseConfig { - return ObjectMapper(YAMLFactory()).registerKotlinModule().readValue(File(configPath)) + fun databaseConfig( + @Value("\${lapis.databaseConfig.path}") configPath: String, + yamlObjectMapper: YamlObjectMapper, + ): DatabaseConfig { + return yamlObjectMapper.objectMapper.readValue(File(configPath)) } @Bean @@ -55,6 +56,7 @@ class LapisSpringConfig { ) @Bean - fun dataOpennessAuthorizationFilter(databaseConfig: DatabaseConfig, objectMapper: ObjectMapper) = - DataOpennessAuthorizationFilter.createFromConfig(databaseConfig, objectMapper) + fun dataOpennessAuthorizationFilter( + dataOpennessAuthorizationFilterFactory: DataOpennessAuthorizationFilterFactory, + ) = dataOpennessAuthorizationFilterFactory.create() } diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/auth/DataOpennessAuthorizationFilter.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/auth/DataOpennessAuthorizationFilter.kt index 8612bffc..770d38d9 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/auth/DataOpennessAuthorizationFilter.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/auth/DataOpennessAuthorizationFilter.kt @@ -1,24 +1,52 @@ package org.genspectrum.lapis.auth import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue import jakarta.servlet.FilterChain import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse +import mu.KotlinLogging +import org.genspectrum.lapis.config.AccessKeys +import org.genspectrum.lapis.config.AccessKeysReader import org.genspectrum.lapis.config.DatabaseConfig import org.genspectrum.lapis.config.OpennessLevel import org.genspectrum.lapis.controller.LapisHttpErrorResponse +import org.genspectrum.lapis.util.CachedBodyHttpServletRequest import org.springframework.http.HttpStatus import org.springframework.http.MediaType +import org.springframework.stereotype.Component import org.springframework.web.filter.OncePerRequestFilter -abstract class DataOpennessAuthorizationFilter(val objectMapper: ObjectMapper) : OncePerRequestFilter() { +const val ACCESS_KEY_PROPERTY = "accessKey" + +private val log = KotlinLogging.logger {} + +@Component +class DataOpennessAuthorizationFilterFactory( + private val databaseConfig: DatabaseConfig, + private val objectMapper: ObjectMapper, + private val accessKeysReader: AccessKeysReader, +) { + fun create() = when (databaseConfig.schema.opennessLevel) { + OpennessLevel.OPEN -> AlwaysAuthorizedAuthorizationFilter(objectMapper) + OpennessLevel.GISAID -> ProtectedGisaidDataAuthorizationFilter( + objectMapper, + accessKeysReader.read(), + databaseConfig.schema.metadata.filter { it.unique }.map { it.name }, + ) + } +} + +abstract class DataOpennessAuthorizationFilter(protected val objectMapper: ObjectMapper) : OncePerRequestFilter() { override fun doFilterInternal( request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain, ) { - when (val result = isAuthorizedForEndpoint(request)) { - AuthorizationResult.Success -> filterChain.doFilter(request, response) + val reReadableRequest = CachedBodyHttpServletRequest(request) + + when (val result = isAuthorizedForEndpoint(reReadableRequest)) { + AuthorizationResult.Success -> filterChain.doFilter(reReadableRequest, response) is AuthorizationResult.Failure -> { response.status = HttpStatus.FORBIDDEN.value() response.contentType = MediaType.APPLICATION_JSON_VALUE @@ -34,15 +62,7 @@ abstract class DataOpennessAuthorizationFilter(val objectMapper: ObjectMapper) : } } - abstract fun isAuthorizedForEndpoint(request: HttpServletRequest): AuthorizationResult - - companion object { - fun createFromConfig(databaseConfig: DatabaseConfig, objectMapper: ObjectMapper) = - when (databaseConfig.schema.opennessLevel) { - OpennessLevel.OPEN -> NoOpAuthorizationFilter(objectMapper) - OpennessLevel.GISAID -> ProtectedGisaidDataAuthorizationFilter(objectMapper) - } - } + abstract fun isAuthorizedForEndpoint(request: CachedBodyHttpServletRequest): AuthorizationResult } sealed interface AuthorizationResult { @@ -52,24 +72,64 @@ sealed interface AuthorizationResult { fun failure(message: String): AuthorizationResult = Failure(message) } - fun isSuccessful(): Boolean - - object Success : AuthorizationResult { - override fun isSuccessful() = true - } + object Success : AuthorizationResult - class Failure(val message: String) : AuthorizationResult { - override fun isSuccessful() = false - } + class Failure(val message: String) : AuthorizationResult } -private class NoOpAuthorizationFilter(objectMapper: ObjectMapper) : DataOpennessAuthorizationFilter(objectMapper) { - override fun isAuthorizedForEndpoint(request: HttpServletRequest) = AuthorizationResult.success() +private class AlwaysAuthorizedAuthorizationFilter(objectMapper: ObjectMapper) : + DataOpennessAuthorizationFilter(objectMapper) { + + override fun isAuthorizedForEndpoint(request: CachedBodyHttpServletRequest) = AuthorizationResult.success() } -private class ProtectedGisaidDataAuthorizationFilter(objectMapper: ObjectMapper) : +private class ProtectedGisaidDataAuthorizationFilter( + objectMapper: ObjectMapper, + private val accessKeys: AccessKeys, + private val fieldsThatServeNonAggregatedData: List, +) : DataOpennessAuthorizationFilter(objectMapper) { - override fun isAuthorizedForEndpoint(request: HttpServletRequest) = - AuthorizationResult.failure("An access key is required to access this endpoint.") + companion object { + private val ENDPOINTS_THAT_SERVE_AGGREGATED_DATA = listOf("/aggregated", "/nucleotideMutations") + } + + override fun isAuthorizedForEndpoint(request: CachedBodyHttpServletRequest): AuthorizationResult { + val requestFields = getRequestFields(request) + + val accessKey = requestFields[ACCESS_KEY_PROPERTY] + ?: return AuthorizationResult.failure("An access key is required to access this endpoint.") + + if (accessKeys.fullAccessKey == accessKey) { + return AuthorizationResult.success() + } + + val endpointServesAggregatedData = ENDPOINTS_THAT_SERVE_AGGREGATED_DATA.contains(request.requestURI) && + fieldsThatServeNonAggregatedData.intersect(requestFields.keys).isEmpty() + + if (endpointServesAggregatedData && accessKeys.aggregatedDataAccessKey == accessKey) { + return AuthorizationResult.success() + } + + return AuthorizationResult.failure("You are not authorized to access this endpoint.") + } + + private fun getRequestFields(request: CachedBodyHttpServletRequest): Map { + if (request.parameterNames.hasMoreElements()) { + return request.parameterMap.mapValues { (_, value) -> value.joinToString() } + } + + if (request.contentLength == 0) { + log.warn { "Could not read access key from body, because content length is 0." } + return emptyMap() + } + + return try { + objectMapper.readValue(request.inputStream) + } catch (exception: Exception) { + log.error { "Failed to read access key from request body: ${exception.message}" } + log.debug { exception.stackTraceToString() } + emptyMap() + } + } } diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/config/AccessKeys.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/config/AccessKeys.kt new file mode 100644 index 00000000..25eed585 --- /dev/null +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/config/AccessKeys.kt @@ -0,0 +1,23 @@ +package org.genspectrum.lapis.config + +import com.fasterxml.jackson.module.kotlin.readValue +import org.genspectrum.lapis.util.YamlObjectMapper +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import java.io.File + +@Component +class AccessKeysReader( + @Value("\${lapis.accessKeys.path:#{null}}") private val accessKeysFile: String?, + private val yamlObjectMapper: YamlObjectMapper, +) { + fun read(): AccessKeys { + if (accessKeysFile == null) { + throw IllegalArgumentException("Cannot read LAPIS access keys, lapis.accessKeys.path was not set.") + } + + return yamlObjectMapper.objectMapper.readValue(File(accessKeysFile)) + } +} + +data class AccessKeys(val fullAccessKey: String, val aggregatedDataAccessKey: String) diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/config/DatabaseConfig.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/config/DatabaseConfig.kt index 829abd5b..0fe36d76 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/config/DatabaseConfig.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/config/DatabaseConfig.kt @@ -10,7 +10,7 @@ data class DatabaseSchema( val features: List = emptyList(), ) -data class DatabaseMetadata(val name: String, val type: String) +data class DatabaseMetadata(val name: String, val type: String, val unique: Boolean = false) data class DatabaseFeature(val name: String) diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/LapisController.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/LapisController.kt index ae6e11ff..dfa2cec1 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/LapisController.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/LapisController.kt @@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.media.ArraySchema import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse +import org.genspectrum.lapis.auth.ACCESS_KEY_PROPERTY import org.genspectrum.lapis.logging.RequestContext import org.genspectrum.lapis.model.SiloQueryModel import org.genspectrum.lapis.response.AggregatedResponse @@ -27,6 +28,9 @@ private const val DEFAULT_MIN_PROPORTION = 0.05 @RestController class LapisController(private val siloQueryModel: SiloQueryModel, private val requestContext: RequestContext) { + companion object { + private val nonSequenceFilterFields = listOf(MIN_PROPORTION_PROPERTY, ACCESS_KEY_PROPERTY) + } @GetMapping("/aggregated") @LapisAggregatedResponse @@ -41,7 +45,7 @@ class LapisController(private val siloQueryModel: SiloQueryModel, private val re ): AggregatedResponse { requestContext.filter = sequenceFilters - return siloQueryModel.aggregate(sequenceFilters) + return siloQueryModel.aggregate(sequenceFilters.filterKeys { !nonSequenceFilterFields.contains(it) }) } @PostMapping("/aggregated") @@ -53,7 +57,7 @@ class LapisController(private val siloQueryModel: SiloQueryModel, private val re ): AggregatedResponse { requestContext.filter = sequenceFilters - return siloQueryModel.aggregate(sequenceFilters) + return siloQueryModel.aggregate(sequenceFilters.filterKeys { !nonSequenceFilterFields.contains(it) }) } @GetMapping("/nucleotideMutations") @@ -72,7 +76,7 @@ class LapisController(private val siloQueryModel: SiloQueryModel, private val re return siloQueryModel.computeMutationProportions( minProportion, - sequenceFilters.filterKeys { it != MIN_PROPORTION_PROPERTY }, + sequenceFilters.filterKeys { !nonSequenceFilterFields.contains(it) }, ) } @@ -85,9 +89,11 @@ class LapisController(private val siloQueryModel: SiloQueryModel, private val re ): List { requestContext.filter = requestBody - val (minProportions, sequenceFilters) = requestBody.entries.partition { it.key == MIN_PROPORTION_PROPERTY } + val (nonSequenceFilters, sequenceFilters) = requestBody.entries.partition { + nonSequenceFilterFields.contains(it.key) + } - val maybeMinProportion = minProportions.getOrNull(0)?.value + val maybeMinProportion = nonSequenceFilters.find { it.key == MIN_PROPORTION_PROPERTY }?.value val minProportion = try { maybeMinProportion?.toDouble() ?: DEFAULT_MIN_PROPORTION } catch (exception: IllegalArgumentException) { diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/util/CachedBodyHttpServletRequest.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/util/CachedBodyHttpServletRequest.kt new file mode 100644 index 00000000..a1fc28bf --- /dev/null +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/util/CachedBodyHttpServletRequest.kt @@ -0,0 +1,46 @@ +package org.genspectrum.lapis.util + +import jakarta.servlet.ReadListener +import jakarta.servlet.ServletInputStream +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletRequestWrapper +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream + +class CachedBodyHttpServletRequest(request: HttpServletRequest) : HttpServletRequestWrapper(request) { + private val cachedBody: ByteArray by lazy { + val inputStream: InputStream = request.inputStream + val byteArrayOutputStream = ByteArrayOutputStream() + + inputStream.copyTo(byteArrayOutputStream) + byteArrayOutputStream.toByteArray() + } + + @Throws(IOException::class) + override fun getInputStream(): ServletInputStream { + return CachedBodyServletInputStream(ByteArrayInputStream(cachedBody)) + } + + private inner class CachedBodyServletInputStream(private val cachedInputStream: ByteArrayInputStream) : + ServletInputStream() { + + override fun isFinished(): Boolean { + return cachedInputStream.available() == 0 + } + + override fun isReady(): Boolean { + return true + } + + override fun setReadListener(listener: ReadListener) { + throw UnsupportedOperationException("setReadListener is not supported") + } + + @Throws(IOException::class) + override fun read(): Int { + return cachedInputStream.read() + } + } +} diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/util/YamlObjectMapper.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/util/YamlObjectMapper.kt new file mode 100644 index 00000000..a7705f2b --- /dev/null +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/util/YamlObjectMapper.kt @@ -0,0 +1,11 @@ +package org.genspectrum.lapis.util + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import org.springframework.stereotype.Component + +@Component +object YamlObjectMapper { + val objectMapper: ObjectMapper = ObjectMapper(YAMLFactory()).registerKotlinModule() +} diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/auth/GisaidAuthorizationTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/auth/GisaidAuthorizationTest.kt index 68e0aefe..d1580d39 100644 --- a/lapis2/src/test/kotlin/org/genspectrum/lapis/auth/GisaidAuthorizationTest.kt +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/auth/GisaidAuthorizationTest.kt @@ -2,9 +2,9 @@ package org.genspectrum.lapis.auth import com.ninjasquad.springmockk.MockkBean import io.mockk.MockKAnnotations -import io.mockk.MockKMatcherScope import io.mockk.every -import org.genspectrum.lapis.controller.LapisController +import io.mockk.verify +import org.genspectrum.lapis.model.SiloQueryModel import org.genspectrum.lapis.response.AggregatedResponse import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -21,20 +21,19 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers class GisaidAuthorizationTest(@Autowired val mockMvc: MockMvc) { @MockkBean - lateinit var lapisController: LapisController + lateinit var siloQueryModelMock: SiloQueryModel - private fun MockKMatcherScope.validControllerCall() = lapisController.aggregated(any()) private val validRoute = "/aggregated" @BeforeEach fun setUp() { - every { validControllerCall() } returns AggregatedResponse(1) + every { siloQueryModelMock.aggregate(any()) } returns AggregatedResponse(1) MockKAnnotations.init(this) } @Test - fun `given no access key in request to GISAID instance, then access is denied`() { + fun `given no access key in GET request to GISAID instance, then access is denied`() { mockMvc.perform(MockMvcRequestBuilders.get(validRoute)) .andExpect(MockMvcResultMatchers.status().isForbidden) .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) @@ -49,4 +48,157 @@ class GisaidAuthorizationTest(@Autowired val mockMvc: MockMvc) { ), ) } + + @Test + fun `given no access key in POST request to GISAID instance, then access is denied`() { + mockMvc.perform(postRequestWithBody("")) + .andExpect(MockMvcResultMatchers.status().isForbidden) + .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andExpect( + MockMvcResultMatchers.content().json( + """ + { + "title": "Forbidden", + "message": "An access key is required to access this endpoint." + } + """, + ), + ) + } + + @Test + fun `given wrong access key in GET request to GISAID instance, then access is denied`() { + mockMvc.perform(MockMvcRequestBuilders.get("$validRoute?accessKey=invalidKey")) + .andExpect(MockMvcResultMatchers.status().isForbidden) + .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andExpect( + MockMvcResultMatchers.content().json( + """ + { + "title": "Forbidden", + "message": "You are not authorized to access this endpoint." + } + """, + ), + ) + } + + @Test + fun `given wrong access key in POST request to GISAID instance, then access is denied`() { + mockMvc.perform(postRequestWithBody("""{"accessKey": "invalidKey"}""")) + .andExpect(MockMvcResultMatchers.status().isForbidden) + .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andExpect( + MockMvcResultMatchers.content().json( + """ + { + "title": "Forbidden", + "message": "You are not authorized to access this endpoint." + } + """, + ), + ) + } + + @Test + fun `given valid access key for aggregated data in GET request to GISAID instance, then access is granted`() { + mockMvc.perform( + MockMvcRequestBuilders.get("$validRoute?accessKey=testAggregatedDataAccessKey&field1=value1"), + ) + .andExpect(MockMvcResultMatchers.status().isOk) + .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + + verify { siloQueryModelMock.aggregate(mapOf("field1" to "value1")) } + } + + @Test + fun `given valid access key for aggregated data in POST request to GISAID instance, then access is granted`() { + mockMvc.perform( + postRequestWithBody( + """ { + "accessKey": "testAggregatedDataAccessKey", + "field1": "value1" + }""", + ), + ) + .andExpect(MockMvcResultMatchers.status().isOk) + .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + + verify { siloQueryModelMock.aggregate(mapOf("field1" to "value1")) } + } + + @Test + fun `given aggregated access key in GET request but filters are too fine-grained, then access is denied`() { + mockMvc.perform( + MockMvcRequestBuilders.get("$validRoute?accessKey=testAggregatedDataAccessKey&gisaid_epi_isl=value"), + ) + .andExpect(MockMvcResultMatchers.status().isForbidden) + .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andExpect( + MockMvcResultMatchers.content().json( + """ + { + "title": "Forbidden", + "message": "You are not authorized to access this endpoint." + } + """, + ), + ) + } + + @Test + fun `given aggregated access key in POST request but filters are too fine-grained, then access is denied`() { + mockMvc.perform( + postRequestWithBody( + """ { + "accessKey": "testAggregatedDataAccessKey", + "gisaid_epi_isl": "some value" + }""", + ), + ) + .andExpect(MockMvcResultMatchers.status().isForbidden) + .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andExpect( + MockMvcResultMatchers.content().json( + """ + { + "title": "Forbidden", + "message": "You are not authorized to access this endpoint." + } + """, + ), + ) + } + + @Test + fun `given valid access key for full access in GET request to GISAID instance, then access is granted`() { + mockMvc.perform( + MockMvcRequestBuilders.get("$validRoute?accessKey=testFullAccessKey&field1=value1"), + ) + .andExpect(MockMvcResultMatchers.status().isOk) + .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + + verify { siloQueryModelMock.aggregate(mapOf("field1" to "value1")) } + } + + @Test + fun `given valid access key for full access in POST request to GISAID instance, then access is granted`() { + mockMvc.perform( + postRequestWithBody( + """ { + "accessKey": "testFullAccessKey", + "field1": "value1" + }""", + ), + ) + .andExpect(MockMvcResultMatchers.status().isOk) + .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + + verify { siloQueryModelMock.aggregate(mapOf("field1" to "value1")) } + } + + private fun postRequestWithBody(body: String) = + MockMvcRequestBuilders.post(validRoute) + .contentType(MediaType.APPLICATION_JSON) + .content(body) } diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/config/AccessKeysReaderTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/config/AccessKeysReaderTest.kt new file mode 100644 index 00000000..a19edac6 --- /dev/null +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/config/AccessKeysReaderTest.kt @@ -0,0 +1,36 @@ +package org.genspectrum.lapis.config + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.`is` +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ActiveProfiles + +@SpringBootTest +class AccessKeysReaderTest { + @Autowired + lateinit var underTest: AccessKeysReader + + @Test + fun `given access keys file path as property then should successfully read access keys`() { + val result = underTest.read() + + assertThat(result.fullAccessKey, `is`(equalTo("testFullAccessKey"))) + assertThat(result.aggregatedDataAccessKey, `is`(equalTo("testAggregatedDataAccessKey"))) + } +} + +@SpringBootTest +@ActiveProfiles("testWithoutAccessKeys") +class AccessKeysReaderWithPathNotSetTest { + @Autowired + lateinit var underTest: AccessKeysReader + + @Test + fun `given access keys file path property is not set then should throw exception when reading access keys`() { + assertThrows { underTest.read() } + } +} diff --git a/lapis2/src/test/resources/application-test.properties b/lapis2/src/test/resources/application-test.properties index 7bb53e9e..9665721e 100644 --- a/lapis2/src/test/resources/application-test.properties +++ b/lapis2/src/test/resources/application-test.properties @@ -1,2 +1,3 @@ silo.url=http://url.to.silo lapis.databaseConfig.path=src/test/resources/config/testDatabaseConfig.yaml +lapis.accessKeys.path=src/test/resources/config/testAccessKeys.yaml diff --git a/lapis2/src/test/resources/application-testWithoutAccessKeys.properties b/lapis2/src/test/resources/application-testWithoutAccessKeys.properties new file mode 100644 index 00000000..fd626d23 --- /dev/null +++ b/lapis2/src/test/resources/application-testWithoutAccessKeys.properties @@ -0,0 +1,4 @@ +spring.config.import=file:src/main/resources/application.properties + +silo.url=http://url.to.silo +lapis.databaseConfig.path=src/test/resources/config/testDatabaseConfig.yaml diff --git a/lapis2/src/test/resources/config/gisaidDatabaseConfig.yaml b/lapis2/src/test/resources/config/gisaidDatabaseConfig.yaml index 5c62a2bc..f6507a5c 100644 --- a/lapis2/src/test/resources/config/gisaidDatabaseConfig.yaml +++ b/lapis2/src/test/resources/config/gisaidDatabaseConfig.yaml @@ -2,6 +2,9 @@ schema: instanceName: gisaidTestConfig opennessLevel: GISAID metadata: - - name: pangoLineage - type: pango_lineage - primaryKey: pangoLineage + - name: gisaid_epi_isl + type: string + unique: true + - name: country + type: string + primaryKey: gisaid_epi_isl diff --git a/lapis2/src/test/resources/config/testAccessKeys.yaml b/lapis2/src/test/resources/config/testAccessKeys.yaml new file mode 100644 index 00000000..505988ab --- /dev/null +++ b/lapis2/src/test/resources/config/testAccessKeys.yaml @@ -0,0 +1,2 @@ +fullAccessKey: testFullAccessKey +aggregatedDataAccessKey: testAggregatedDataAccessKey diff --git a/lapis2/src/test/resources/config/testDatabaseConfigWithoutFeatures.yaml b/lapis2/src/test/resources/config/testDatabaseConfigWithoutFeatures.yaml index e0bfe7bd..94c23e30 100644 --- a/lapis2/src/test/resources/config/testDatabaseConfigWithoutFeatures.yaml +++ b/lapis2/src/test/resources/config/testDatabaseConfigWithoutFeatures.yaml @@ -1,5 +1,6 @@ schema: instanceName: sars_cov-2_minimal_test_config + opennessLevel: OPEN metadata: - name: gisaid_epi_isl type: string