Skip to content

Commit

Permalink
feat: base implementation to provide the openness level in the config
Browse files Browse the repository at this point in the history
issue: #218
  • Loading branch information
fengelniederhammer committed May 2, 2023
1 parent b68576a commit aff116c
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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.config.DatabaseConfig
import org.genspectrum.lapis.config.SequenceFilterFields
import org.genspectrum.lapis.logging.RequestContext
Expand Down Expand Up @@ -52,4 +53,8 @@ class LapisSpringConfig {
KotlinLogging.logger("StatisticsLogger"),
timeFactory,
)

@Bean
fun dataOpennessAuthorizationFilter(databaseConfig: DatabaseConfig, objectMapper: ObjectMapper) =
DataOpennessAuthorizationFilter.createFromConfig(databaseConfig, objectMapper)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package org.genspectrum.lapis.auth

import com.fasterxml.jackson.databind.ObjectMapper
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.genspectrum.lapis.config.DatabaseConfig
import org.genspectrum.lapis.config.OpennessLevel
import org.genspectrum.lapis.controller.LapisHttpErrorResponse
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.web.filter.OncePerRequestFilter

abstract class DataOpennessAuthorizationFilter(val objectMapper: ObjectMapper) : OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain,
) {
when (val result = isAuthorizedForEndpoint(request)) {
AuthorizationResult.Success -> filterChain.doFilter(request, response)
is AuthorizationResult.Failure -> {
response.status = HttpStatus.FORBIDDEN.value()
response.contentType = MediaType.APPLICATION_JSON_VALUE
response.writer.write(
objectMapper.writeValueAsString(
LapisHttpErrorResponse(
"Forbidden",
result.message,
),
),
)
}
}
}

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)
}
}
}

sealed interface AuthorizationResult {
companion object {
fun success(): AuthorizationResult = Success

fun failure(message: String): AuthorizationResult = Failure(message)
}

fun isSuccessful(): Boolean

object Success : AuthorizationResult {
override fun isSuccessful() = true
}

class Failure(val message: String) : AuthorizationResult {
override fun isSuccessful() = false
}
}

private class NoOpAuthorizationFilter(objectMapper: ObjectMapper) : DataOpennessAuthorizationFilter(objectMapper) {
override fun isAuthorizedForEndpoint(request: HttpServletRequest) = AuthorizationResult.success()
}

private class ProtectedGisaidDataAuthorizationFilter(objectMapper: ObjectMapper) :
DataOpennessAuthorizationFilter(objectMapper) {

override fun isAuthorizedForEndpoint(request: HttpServletRequest) =
AuthorizationResult.failure("An access key is required to access this endpoint.")
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ data class DatabaseConfig(val schema: DatabaseSchema)

data class DatabaseSchema(
val instanceName: String,
val opennessLevel: OpennessLevel,
val metadata: List<DatabaseMetadata>,
val primaryKey: String,
val features: List<DatabaseFeature> = emptyList(),
Expand All @@ -12,3 +13,8 @@ data class DatabaseSchema(
data class DatabaseMetadata(val name: String, val type: String)

data class DatabaseFeature(val name: String)

enum class OpennessLevel {
OPEN,
GISAID,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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 org.genspectrum.lapis.response.AggregatedResponse
import org.junit.jupiter.api.BeforeEach
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.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.result.MockMvcResultMatchers

@SpringBootTest(properties = ["lapis.databaseConfig.path=src/test/resources/config/gisaidDatabaseConfig.yaml"])
@AutoConfigureMockMvc
class GisaidAuthorizationTest(@Autowired val mockMvc: MockMvc) {

@MockkBean
lateinit var lapisController: LapisController

private fun MockKMatcherScope.validControllerCall() = lapisController.aggregated(any())
private val validRoute = "/aggregated"

@BeforeEach
fun setUp() {
every { validControllerCall() } returns AggregatedResponse(1)

MockKAnnotations.init(this)
}

@Test
fun `given no access key in request to GISAID instance, then access is denied`() {
mockMvc.perform(MockMvcRequestBuilders.get(validRoute))
.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."
}
""",
),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class DatabaseConfigTest {
fun `load test database config`() {
assertThat(underTest.schema.instanceName, `is`("sars_cov-2_minimal_test_config"))
assertThat(underTest.schema.primaryKey, `is`("gisaid_epi_isl"))
assertThat(underTest.schema.opennessLevel, `is`(OpennessLevel.OPEN))
assertThat(
underTest.schema.metadata,
containsInAnyOrder(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class SequenceFilterFieldsTest {
) = DatabaseConfig(
DatabaseSchema(
"test config",
OpennessLevel.OPEN,
databaseMetadata,
"test primary key",
databaseFeatures,
Expand Down
7 changes: 7 additions & 0 deletions lapis2/src/test/resources/config/gisaidDatabaseConfig.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
schema:
instanceName: gisaidTestConfig
opennessLevel: GISAID
metadata:
- name: pangoLineage
type: pango_lineage
primaryKey: pangoLineage
1 change: 1 addition & 0 deletions lapis2/src/test/resources/config/testDatabaseConfig.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
schema:
instanceName: sars_cov-2_minimal_test_config
opennessLevel: OPEN
metadata:
- name: gisaid_epi_isl
type: string
Expand Down
1 change: 1 addition & 0 deletions siloLapisTests/testDatabaseConfig.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
schema:
instanceName: sars_cov-2_minimal_test_config
opennessLevel: OPEN
metadata:
- name: gisaid_epi_isl
type: string
Expand Down

0 comments on commit aff116c

Please sign in to comment.