From 0cd307047218bcc8ceb6890ba237df8a4bfe9976 Mon Sep 17 00:00:00 2001 From: Fabian Engelniederhammer Date: Tue, 25 Apr 2023 12:00:09 +0200 Subject: [PATCH] feat: document access key in swagger docs issue: #218 --- .../genspectrum/lapis/LapisSpringConfig.kt | 3 +- .../org/genspectrum/lapis/OpenApiDocs.kt | 18 +++++++++-- .../auth/DataOpennessAuthorizationFilter.kt | 9 ++++-- .../lapis/controller/LapisController.kt | 10 +++++++ .../lapis/auth/GisaidAuthorizationTest.kt | 30 +++++++++++++++---- 5 files changed, 59 insertions(+), 11 deletions(-) diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/LapisSpringConfig.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/LapisSpringConfig.kt index 4da0888a..50714d59 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/LapisSpringConfig.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/LapisSpringConfig.kt @@ -19,7 +19,8 @@ import java.io.File @Configuration class LapisSpringConfig { @Bean - fun openAPI(sequenceFilterFields: SequenceFilterFields) = buildOpenApiSchema(sequenceFilterFields) + fun openAPI(sequenceFilterFields: SequenceFilterFields, databaseConfig: DatabaseConfig) = + buildOpenApiSchema(sequenceFilterFields, databaseConfig) @Bean fun databaseConfig( diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/OpenApiDocs.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/OpenApiDocs.kt index 7b417dff..4481c99c 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/OpenApiDocs.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/OpenApiDocs.kt @@ -3,16 +3,22 @@ package org.genspectrum.lapis import io.swagger.v3.oas.models.Components import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.media.Schema +import org.genspectrum.lapis.config.DatabaseConfig +import org.genspectrum.lapis.config.OpennessLevel import org.genspectrum.lapis.config.SequenceFilterFields import org.genspectrum.lapis.controller.MIN_PROPORTION_PROPERTY import org.genspectrum.lapis.controller.REQUEST_SCHEMA import org.genspectrum.lapis.controller.REQUEST_SCHEMA_WITH_MIN_PROPORTION -fun buildOpenApiSchema(sequenceFilterFields: SequenceFilterFields): OpenAPI { - val properties = sequenceFilterFields.fields +fun buildOpenApiSchema(sequenceFilterFields: SequenceFilterFields, databaseConfig: DatabaseConfig): OpenAPI { + var properties = sequenceFilterFields.fields .map { (fieldName, fieldType) -> fieldName to Schema().type(fieldType.openApiType) } .toMap() + if (databaseConfig.schema.opennessLevel == OpennessLevel.GISAID) { + properties = properties + ("accessKey" to accessKeySchema) + } + return OpenAPI() .components( Components().addSchemas( @@ -30,3 +36,11 @@ fun buildOpenApiSchema(sequenceFilterFields: SequenceFilterFields): OpenAPI { ), ) } + +private val accessKeySchema = Schema() + .type("string") + .description( + "An access key that grants access to the protected data that this instance serves. " + + "There are two types or access keys: One only grants access to aggregated data, " + + "the other also grants access to detailed data.", + ) 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 770d38d9..e33d8e21 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/auth/DataOpennessAuthorizationFilter.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/auth/DataOpennessAuthorizationFilter.kt @@ -91,14 +91,19 @@ private class ProtectedGisaidDataAuthorizationFilter( DataOpennessAuthorizationFilter(objectMapper) { companion object { + private val WHITELISTED_PATHS = listOf("/swagger-ui", "/api-docs") private val ENDPOINTS_THAT_SERVE_AGGREGATED_DATA = listOf("/aggregated", "/nucleotideMutations") } override fun isAuthorizedForEndpoint(request: CachedBodyHttpServletRequest): AuthorizationResult { + if (WHITELISTED_PATHS.any { request.requestURI.startsWith(it) }) { + return AuthorizationResult.success() + } + val requestFields = getRequestFields(request) val accessKey = requestFields[ACCESS_KEY_PROPERTY] - ?: return AuthorizationResult.failure("An access key is required to access this endpoint.") + ?: return AuthorizationResult.failure("An access key is required to access ${request.requestURI}.") if (accessKeys.fullAccessKey == accessKey) { return AuthorizationResult.success() @@ -111,7 +116,7 @@ private class ProtectedGisaidDataAuthorizationFilter( return AuthorizationResult.success() } - return AuthorizationResult.failure("You are not authorized to access this endpoint.") + return AuthorizationResult.failure("You are not authorized to access ${request.requestURI}.") } private fun getRequestFields(request: CachedBodyHttpServletRequest): Map { 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 dfa2cec1..74d80c9c 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/LapisController.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/LapisController.kt @@ -124,6 +124,11 @@ class LapisController(private val siloQueryModel: SiloQueryModel, private val re description = "Bad Request", content = [Content(schema = Schema(implementation = LapisHttpErrorResponse::class))], ), + ApiResponse( + responseCode = "403", + description = "Forbidden", + content = [Content(schema = Schema(implementation = LapisHttpErrorResponse::class))], + ), ApiResponse( responseCode = "500", description = "Internal Server Error", @@ -149,6 +154,11 @@ private annotation class LapisAggregatedResponse description = "Bad Request", content = [Content(schema = Schema(implementation = LapisHttpErrorResponse::class))], ), + ApiResponse( + responseCode = "403", + description = "Forbidden", + content = [Content(schema = Schema(implementation = LapisHttpErrorResponse::class))], + ), ApiResponse( responseCode = "500", description = "Internal Server Error", 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 d1580d39..3ec9d4b1 100644 --- a/lapis2/src/test/kotlin/org/genspectrum/lapis/auth/GisaidAuthorizationTest.kt +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/auth/GisaidAuthorizationTest.kt @@ -42,7 +42,7 @@ class GisaidAuthorizationTest(@Autowired val mockMvc: MockMvc) { """ { "title": "Forbidden", - "message": "An access key is required to access this endpoint." + "message": "An access key is required to access /aggregated." } """, ), @@ -59,7 +59,7 @@ class GisaidAuthorizationTest(@Autowired val mockMvc: MockMvc) { """ { "title": "Forbidden", - "message": "An access key is required to access this endpoint." + "message": "An access key is required to access /aggregated." } """, ), @@ -76,7 +76,7 @@ class GisaidAuthorizationTest(@Autowired val mockMvc: MockMvc) { """ { "title": "Forbidden", - "message": "You are not authorized to access this endpoint." + "message": "You are not authorized to access /aggregated." } """, ), @@ -93,7 +93,7 @@ class GisaidAuthorizationTest(@Autowired val mockMvc: MockMvc) { """ { "title": "Forbidden", - "message": "You are not authorized to access this endpoint." + "message": "You are not authorized to access /aggregated." } """, ), @@ -139,7 +139,7 @@ class GisaidAuthorizationTest(@Autowired val mockMvc: MockMvc) { """ { "title": "Forbidden", - "message": "You are not authorized to access this endpoint." + "message": "You are not authorized to access /aggregated." } """, ), @@ -163,7 +163,7 @@ class GisaidAuthorizationTest(@Autowired val mockMvc: MockMvc) { """ { "title": "Forbidden", - "message": "You are not authorized to access this endpoint." + "message": "You are not authorized to access /aggregated." } """, ), @@ -197,6 +197,24 @@ class GisaidAuthorizationTest(@Autowired val mockMvc: MockMvc) { verify { siloQueryModelMock.aggregate(mapOf("field1" to "value1")) } } + @Test + fun `the swagger ui and api docs are always accessible`() { + mockMvc.perform( + MockMvcRequestBuilders.get("/swagger-ui/index.html"), + ) + .andExpect(MockMvcResultMatchers.status().isOk) + + mockMvc.perform( + MockMvcRequestBuilders.get("/api-docs"), + ) + .andExpect(MockMvcResultMatchers.status().isOk) + + mockMvc.perform( + MockMvcRequestBuilders.get("/api-docs.yaml"), + ) + .andExpect(MockMvcResultMatchers.status().isOk) + } + private fun postRequestWithBody(body: String) = MockMvcRequestBuilders.post(validRoute) .contentType(MediaType.APPLICATION_JSON)