diff --git a/.github/workflows/lapis.yml b/.github/workflows/lapis.yml index 892542226..98414e974 100644 --- a/.github/workflows/lapis.yml +++ b/.github/workflows/lapis.yml @@ -141,7 +141,6 @@ jobs: docker compose logs siloMultisegmented > e2e-logs/siloMultisegmented.log docker compose logs siloPreprocessingMultisegmented > e2e-logs/siloPreprocessingMultisegmented.log docker compose logs lapisOpen > e2e-logs/lapisOpen.log - docker compose logs lapisProtected > e2e-logs/lapisProtected.log docker compose logs lapisMultiSegmented > e2e-logs/lapisMultiSegmented.log env: SILO_TAG: 0.8.5 diff --git a/.idea/runConfigurations/LapisProtected.xml b/.idea/runConfigurations/LapisProtected.xml deleted file mode 100644 index 8ba408814..000000000 --- a/.idea/runConfigurations/LapisProtected.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - \ No newline at end of file diff --git a/lapis-docs/src/components/ConfigGenerator/configContext.tsx b/lapis-docs/src/components/ConfigGenerator/configContext.tsx index 04f8ace6e..85a65714a 100644 --- a/lapis-docs/src/components/ConfigGenerator/configContext.tsx +++ b/lapis-docs/src/components/ConfigGenerator/configContext.tsx @@ -4,9 +4,8 @@ import { z } from 'zod'; export type ConfigType = 'SILO' | 'Pathoplexus'; export const LAPIS_OPENNESS_OPEN = 'OPEN'; -export const LAPIS_OPENNESS_PROTECTED = 'PROTECTED'; -export const opennessLevelSchema = z.enum([LAPIS_OPENNESS_OPEN, LAPIS_OPENNESS_PROTECTED]); +export const opennessLevelSchema = z.enum([LAPIS_OPENNESS_OPEN]); export type OpennessLevel = z.infer; export const metadataTypeSchema = z.enum([ diff --git a/lapis-e2e/.prettierignore b/lapis-e2e/.prettierignore index df9332181..161c841f2 100644 --- a/lapis-e2e/.prettierignore +++ b/lapis-e2e/.prettierignore @@ -1,3 +1,2 @@ /test/lapisClient -/test/lapisClientProtected /test/lapisClientMultiSegmented \ No newline at end of file diff --git a/lapis-e2e/generateOpenApiClients.sh b/lapis-e2e/generateOpenApiClients.sh index 0d25648f5..7a58ee5bb 100755 --- a/lapis-e2e/generateOpenApiClients.sh +++ b/lapis-e2e/generateOpenApiClients.sh @@ -4,10 +4,8 @@ set -euo pipefail cd "../lapis" ./gradlew generateOpenApiDocs -./gradlew generateOpenApiDocs -PopennessLevel=protected ./gradlew generateOpenApiDocs -Psegmented=true cd - npm run generateLapisClient -npm run generateLapisClientProtected npm run generateLapisClientMultiSegmented diff --git a/lapis-e2e/package.json b/lapis-e2e/package.json index e559ff6a2..3f588d9c0 100644 --- a/lapis-e2e/package.json +++ b/lapis-e2e/package.json @@ -7,14 +7,10 @@ "generateLapisClient": "npm run runOpenApiGenerator && npm run copyGeneratedFiles && npm run cleanUpGeneratedFiles", "runOpenApiGenerator": "openapi-generator-cli generate -i ../lapis/lapis-openapi-single-segmented.json -g typescript-fetch -o generated-sources", "copyGeneratedFiles": "mkdir -p test/lapisClient && cp generated-sources/index.ts generated-sources/runtime.ts test/lapisClient && cp -r generated-sources/apis generated-sources/models test/lapisClient", - "generateLapisClientProtected": "npm run runOpenApiGeneratorProtected && npm run copyGeneratedFilesProtected && npm run cleanUpGeneratedFilesProtected", - "runOpenApiGeneratorProtected": "openapi-generator-cli generate -i ../lapis/lapis-openapi-single-segmented-protected.json -g typescript-fetch -o generated-sources-protected", - "copyGeneratedFilesProtected": "mkdir -p test/lapisClientProtected && cp generated-sources-protected/index.ts generated-sources-protected/runtime.ts test/lapisClientProtected && cp -r generated-sources-protected/apis generated-sources-protected/models test/lapisClientProtected", "generateLapisClientMultiSegmented": "npm run runOpenApiGeneratorMultiSegmented && npm run copyGeneratedFilesMultiSegmented && npm run cleanUpGeneratedFilesMultiSegmented", "runOpenApiGeneratorMultiSegmented": "openapi-generator-cli generate -i ../lapis/lapis-openapi-multi-segmented.json -g typescript-fetch -o generated-sources-multi-segmented", "copyGeneratedFilesMultiSegmented": "mkdir -p test/lapisClientMultiSegmented && cp generated-sources-multi-segmented/index.ts generated-sources-multi-segmented/runtime.ts test/lapisClientMultiSegmented && cp -r generated-sources-multi-segmented/apis generated-sources-multi-segmented/models test/lapisClientMultiSegmented", "cleanUpGeneratedFiles": "rm -rf generated-sources", - "cleanUpGeneratedFilesProtected": "rm -rf generated-sources-protected", "cleanUpGeneratedFilesMultiSegmented": "rm -rf generated-sources-multi-segmented", "check-format": "prettier --check .", "format": "prettier --write ." diff --git a/lapis-e2e/test/common.ts b/lapis-e2e/test/common.ts index 5722ac766..be1243413 100644 --- a/lapis-e2e/test/common.ts +++ b/lapis-e2e/test/common.ts @@ -7,17 +7,13 @@ import { SingleSegmentedSequenceControllerApi, } from './lapisClient'; import { MutationsOverTimeControllerApi } from './lapisClient/index'; -import { LapisControllerApi as LapisControllerApiMultiSegmented } from './lapisClientMultiSegmented'; -import { expect } from 'chai'; - import { - LapisControllerApi as LapisControllerApiProtected, - Configuration as ConfigurationProtected, -} from './lapisClientProtected'; -import { MultiSegmentedSequenceControllerApi } from './lapisClientMultiSegmented'; + LapisControllerApi as LapisControllerApiMultiSegmented, + MultiSegmentedSequenceControllerApi, +} from './lapisClientMultiSegmented'; +import { expect } from 'chai'; export const basePath = 'http://localhost:8090'; -export const basePathProtected = 'http://localhost:8092'; export const basePathMultiSegmented = 'http://localhost:8094'; const middleware: Middleware = { @@ -51,9 +47,6 @@ export const actuatorClient = new ActuatorApi(new Configuration({ basePath })).w export const mutOverTimeClient = new MutationsOverTimeControllerApi( new Configuration({ basePath }) ).withMiddleware(middleware); -export const lapisClientProtected = new LapisControllerApiProtected( - new ConfigurationProtected({ basePath: basePathProtected }) -).withMiddleware(middleware); export const lapisClientMultiSegmented = new LapisControllerApiMultiSegmented( new Configuration({ basePath: basePathMultiSegmented }) ).withMiddleware(middleware); diff --git a/lapis-e2e/test/lapisClientProtected/.gitignore b/lapis-e2e/test/lapisClientProtected/.gitignore deleted file mode 100644 index 72e8ffc0d..000000000 --- a/lapis-e2e/test/lapisClientProtected/.gitignore +++ /dev/null @@ -1 +0,0 @@ -* diff --git a/lapis-e2e/test/protectedRoutes.spec.ts b/lapis-e2e/test/protectedRoutes.spec.ts deleted file mode 100644 index 25480dc61..000000000 --- a/lapis-e2e/test/protectedRoutes.spec.ts +++ /dev/null @@ -1,133 +0,0 @@ -import crypto from 'crypto'; -import { expect } from 'chai'; -import { basePathProtected, lapisClientProtected } from './common'; - -const aggregatedAccessKey = 'testAggregatedDataAccessKey'; -const fullAccessKey = 'testFullAccessKey'; - -function hashAccessKey(baseKey: string): string { - const epoch = Math.floor(Date.now() / 1000); - return crypto.createHash('sha256').update(`${baseKey}:${epoch}`).digest('hex'); -} - -describe('Protected mode on GET requests', () => { - it('should deny access, when no access key is provided', async () => { - const result = await fetch(basePathProtected + '/sample/aggregated'); - - expect(result.status).equals(403); - expect(result.headers.get('Content-Type')).equals('application/json;charset=ISO-8859-1'); - expect((await result.json()).error).to.deep.equal({ - detail: 'An access key is required to access /sample/aggregated.', - status: 403, - title: 'Forbidden', - type: 'about:blank', - }); - }); - - it('should deny access, when wrong access key is provided', async () => { - const invalidAccessKey = 'invalidKey'; - const result = await fetch(`${basePathProtected}/sample/aggregated?accessKey=${invalidAccessKey}`); - - expect(result.status).equals(403); - expect(result.headers.get('Content-Type')).equals('application/json;charset=ISO-8859-1'); - expect((await result.json()).error).to.deep.equal({ - detail: 'You are not authorized to access /sample/aggregated.', - status: 403, - title: 'Forbidden', - type: 'about:blank', - }); - }); - - it('should deny access, when providing aggregated access key on non aggregated route', async () => { - const result = await fetch( - `${basePathProtected}/sample/details?accessKey=${hashAccessKey(aggregatedAccessKey)}` - ); - - expect(result.status).equals(403); - expect(result.headers.get('Content-Type')).equals('application/json;charset=ISO-8859-1'); - expect((await result.json()).error).to.deep.equal({ - detail: 'You are not authorized to access /sample/details.', - status: 403, - title: 'Forbidden', - type: 'about:blank', - }); - }); - - it('should grant access, when providing aggregated access key', async () => { - const result = await fetch( - `${basePathProtected}/sample/aggregated?accessKey=${hashAccessKey(aggregatedAccessKey)}` - ); - - expect(result.status).equals(200); - }); - - it('should grant access, when providing full access key', async () => { - const result = await fetch(`${basePathProtected}/sample/aggregated?accessKey=${fullAccessKey}`); - - expect(result.status).equals(200); - }); - - it('should grant access, when providing full access key on non aggregated route', async () => { - const result = await fetch(`${basePathProtected}/sample/details?accessKey=${fullAccessKey}`); - - expect(result.status).equals(200); - }); -}); - -describe('Protected mode on POST requests', () => { - it('should deny access, when no access key is provided', async () => { - const result = lapisClientProtected.postAggregated({ aggregatedPostRequest: {} }); - - await expectResponseStatusIs(result, 403); - }); - - it('should deny access, when wrong access key is provided', async () => { - const invalidAccessKey = 'invalidKey'; - const result = lapisClientProtected.postAggregated({ - aggregatedPostRequest: { accessKey: invalidAccessKey }, - }); - - await expectResponseStatusIs(result, 403); - }); - - it('should deny access, when providing aggregated access key on non aggregated route', async () => { - const result = lapisClientProtected.postDetails({ - detailsPostRequest: { accessKey: hashAccessKey(aggregatedAccessKey) }, - }); - - await expectResponseStatusIs(result, 403); - }); - - it('should grant access, when providing aggregated access key', async () => { - const result = lapisClientProtected.postAggregated({ - aggregatedPostRequest: { accessKey: hashAccessKey(aggregatedAccessKey) }, - }); - - expect(await result).to.be.ok; - }); - - it('should grant access, when providing full access key', async () => { - const result = lapisClientProtected.postAggregated({ - aggregatedPostRequest: { accessKey: fullAccessKey }, - }); - - expect(await result).to.be.ok; - }); - - it('should grant access, when providing full access key on non aggregated route', async () => { - const result = lapisClientProtected.postDetails({ - detailsPostRequest: { accessKey: fullAccessKey }, - }); - - expect(await result).to.be.ok; - }); - - async function expectResponseStatusIs(response: Promise, expectedStatus: number) { - try { - const success = await response; - expect.fail('Expected response to be rejected, but was ' + JSON.stringify(success)); - } catch (e) { - expect(e).to.have.property('response').that.has.property('status', expectedStatus); - } - } -}); diff --git a/lapis-e2e/testData/singleSegmented/protectedTestDatabaseConfig.yaml b/lapis-e2e/testData/singleSegmented/protectedTestDatabaseConfig.yaml deleted file mode 100644 index 5ee5af904..000000000 --- a/lapis-e2e/testData/singleSegmented/protectedTestDatabaseConfig.yaml +++ /dev/null @@ -1,33 +0,0 @@ -schema: - instanceName: sars_cov-2_minimal_test_config - opennessLevel: PROTECTED - metadata: - - name: primaryKey - type: string - - name: usherTree - type: string - isPhyloTreeField: true - - name: date - type: date - - name: region - type: string - generateIndex: true - - name: country - type: string - generateIndex: true - - name: pangoLineage - type: string - generateIndex: true - generateLineageIndex: lineage_definitions - - name: division - type: string - generateIndex: true - - name: age - type: int - - name: qc_value - type: float - - name: test_boolean_column - type: boolean - features: - - name: sarsCoV2VariantQuery - primaryKey: primaryKey diff --git a/lapis/.gitignore b/lapis/.gitignore index fcddde85d..5422fbbea 100644 --- a/lapis/.gitignore +++ b/lapis/.gitignore @@ -4,9 +4,7 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ /lapis-openapi.json -/lapis-openapi-protected.json log/ logs/ /lapis-openapi-multi-segmented.json /lapis-openapi-single-segmented.json -/lapis-openapi-single-segmented-protected.json diff --git a/lapis/README.md b/lapis/README.md index 25742968b..f398131b3 100644 --- a/lapis/README.md +++ b/lapis/README.md @@ -135,9 +135,3 @@ To generate the OpenApi docs for an instance with multi-segmented reference geno ```bash ./gradlew generateOpenApiDocs -Psegmented=true ``` - -To generate the OpenApi docs for a protected instance run -```bash -./gradlew generateOpenApiDocs -PopennessLevel=protected -``` - diff --git a/lapis/build.gradle b/lapis/build.gradle index 6c9d0c4af..4a2c51ef8 100644 --- a/lapis/build.gradle +++ b/lapis/build.gradle @@ -90,24 +90,21 @@ ktlint { openApi { outputDir.set(file("$rootDir")) - def opennessLevel = project.hasProperty("opennessLevel") ? project.opennessLevel : "open" def segmented = project.hasProperty("segmented") ? project.segmented : "false" - def getCustomOutputFileName = { opennessLevel_, segmented_ -> + def getCustomOutputFileName = { segmented_ -> if (segmented_ == "true") { return "lapis-openapi-multi-segmented.json" } else { - return opennessLevel_ == "open" ? "lapis-openapi-single-segmented.json" : "lapis-openapi-single-segmented-protected.json" + return "lapis-openapi-single-segmented.json" } } - def getCustomLapisConfig = { opennessLevel_, segmented_ -> + def getCustomLapisConfig = { segmented_ -> if (segmented_ == "true") { return "$rootDir/../lapis-e2e/testData/multiSegmented/testDatabaseConfig.yaml" } else { - return opennessLevel_ == "open" - ? "$rootDir/../lapis-e2e/testData/singleSegmented/testDatabaseConfig.yaml" - : "$rootDir/../lapis-e2e/testData/singleSegmented/protectedTestDatabaseConfig.yaml" + return "$rootDir/../lapis-e2e/testData/singleSegmented/testDatabaseConfig.yaml" } } @@ -119,8 +116,8 @@ openApi { } } - def customOutputFileName = getCustomOutputFileName(opennessLevel, segmented) - def customLapisConfig = getCustomLapisConfig(opennessLevel, segmented) + def customOutputFileName = getCustomOutputFileName(segmented) + def customLapisConfig = getCustomLapisConfig(segmented) def referenceGenomeFilename = getReferenceGenomeFilename(segmented) outputFileName.set(customOutputFileName) @@ -131,7 +128,6 @@ openApi { "--silo.url=does.not.matter.here", "--lapis.databaseConfig.path=$customLapisConfig", "--referenceGenomeFilename=$referenceGenomeFilename", - "--lapis.accessKeys.path=$rootDir/src/test/resources/config/testAccessKeys.yaml" ]) } } diff --git a/lapis/docker-compose.yml b/lapis/docker-compose.yml index 8556f2ac6..accf5afc1 100644 --- a/lapis/docker-compose.yml +++ b/lapis/docker-compose.yml @@ -74,22 +74,3 @@ services: source: ../lapis-e2e/testData/multiSegmented/reference_genomes.json target: /workspace/reference_genomes.json read_only: true - - lapisProtected: - image: ghcr.io/genspectrum/lapis:${LAPIS_TAG} - platform: linux/amd64 - ports: - - "8092:8080" - command: --silo.url=http://silo:8081 --lapis.accessKeys.path=/workspace/access_keys.yaml - volumes: - - type: bind - source: ../lapis-e2e/testData/singleSegmented/protectedTestDatabaseConfig.yaml - target: /workspace/database_config.yaml - read_only: true - - type: bind - source: ../lapis-e2e/testData/singleSegmented/reference_genomes.json - target: /workspace/reference_genomes.json - read_only: true - - type: bind - source: ./src/test/resources/config/testAccessKeys.yaml - target: /workspace/access_keys.yaml diff --git a/lapis/src/main/kotlin/org/genspectrum/lapis/auth/DataOpennessAuthorizationFilter.kt b/lapis/src/main/kotlin/org/genspectrum/lapis/auth/DataOpennessAuthorizationFilter.kt index 2f135171a..02be0f23c 100644 --- a/lapis/src/main/kotlin/org/genspectrum/lapis/auth/DataOpennessAuthorizationFilter.kt +++ b/lapis/src/main/kotlin/org/genspectrum/lapis/auth/DataOpennessAuthorizationFilter.kt @@ -4,23 +4,9 @@ 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.AccessKeys -import org.genspectrum.lapis.config.AccessKeysReader import org.genspectrum.lapis.config.DatabaseConfig import org.genspectrum.lapis.config.OpennessLevel -import org.genspectrum.lapis.controller.AGGREGATED_ROUTE -import org.genspectrum.lapis.controller.AMINO_ACID_INSERTIONS_ROUTE -import org.genspectrum.lapis.controller.AMINO_ACID_MUTATIONS_OVER_TIME_ROUTE -import org.genspectrum.lapis.controller.AMINO_ACID_MUTATIONS_ROUTE -import org.genspectrum.lapis.controller.DATABASE_CONFIG_ROUTE -import org.genspectrum.lapis.controller.INFO_ROUTE -import org.genspectrum.lapis.controller.NUCLEOTIDE_INSERTIONS_ROUTE -import org.genspectrum.lapis.controller.NUCLEOTIDE_MUTATIONS_OVER_TIME_ROUTE -import org.genspectrum.lapis.controller.NUCLEOTIDE_MUTATIONS_ROUTE -import org.genspectrum.lapis.controller.REFERENCE_GENOME_ROUTE import org.genspectrum.lapis.controller.middleware.DATA_OPENNESS_AUTHORIZATION_FILTER_ORDER -import org.genspectrum.lapis.request.ACCESS_KEY_PROPERTY -import org.genspectrum.lapis.request.FIELDS_PROPERTY import org.genspectrum.lapis.response.LapisErrorResponse import org.genspectrum.lapis.response.LapisInfoFactory import org.genspectrum.lapis.util.CachedBodyHttpServletRequest @@ -30,29 +16,19 @@ import org.springframework.http.MediaType import org.springframework.http.ProblemDetail import org.springframework.stereotype.Component import org.springframework.web.filter.OncePerRequestFilter -import java.security.MessageDigest -import java.time.Instant @Component class DataOpennessAuthorizationFilterFactory( private val databaseConfig: DatabaseConfig, private val objectMapper: ObjectMapper, - private val accessKeysReader: AccessKeysReader, private val lapisInfoFactory: LapisInfoFactory, ) { - fun create() = + fun create(): DataOpennessAuthorizationFilter = when (databaseConfig.schema.opennessLevel) { OpennessLevel.OPEN -> AlwaysAuthorizedAuthorizationFilter( objectMapper = objectMapper, lapisInfoFactory = lapisInfoFactory, ) - - OpennessLevel.PROTECTED -> ProtectedDataAuthorizationFilter( - objectMapper = objectMapper, - lapisInfoFactory = lapisInfoFactory, - accessKeys = accessKeysReader.read(), - databaseConfig = databaseConfig, - ) } } @@ -70,6 +46,7 @@ abstract class DataOpennessAuthorizationFilter( 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 @@ -114,99 +91,3 @@ private class AlwaysAuthorizedAuthorizationFilter( ) : DataOpennessAuthorizationFilter(objectMapper, lapisInfoFactory) { override fun isAuthorizedForEndpoint(request: CachedBodyHttpServletRequest) = AuthorizationResult.success() } - -private class ProtectedDataAuthorizationFilter( - objectMapper: ObjectMapper, - lapisInfoFactory: LapisInfoFactory, - private val accessKeys: AccessKeys, - private val databaseConfig: DatabaseConfig, -) : DataOpennessAuthorizationFilter(objectMapper, lapisInfoFactory) { - private val fieldsThatServeNonAggregatedData = databaseConfig.schema - .metadata - .filter { it.valuesAreUnique } - .map { it.name } - - companion object { - private val WHITELISTED_PATH_PREFIXES = listOf( - "/swagger-ui", - "/api-docs", - "/actuator", - "/sample$DATABASE_CONFIG_ROUTE", - "/sample$REFERENCE_GENOME_ROUTE", - ) - private val ENDPOINTS_THAT_SERVE_AGGREGATED_DATA = listOf( - AGGREGATED_ROUTE, - NUCLEOTIDE_MUTATIONS_ROUTE, - AMINO_ACID_MUTATIONS_ROUTE, - NUCLEOTIDE_INSERTIONS_ROUTE, - AMINO_ACID_INSERTIONS_ROUTE, - INFO_ROUTE, - ).map { "/sample$it" } + listOf( - NUCLEOTIDE_MUTATIONS_OVER_TIME_ROUTE, - AMINO_ACID_MUTATIONS_OVER_TIME_ROUTE, - ).map { "/component$it" } - } - - override fun isAuthorizedForEndpoint(request: CachedBodyHttpServletRequest): AuthorizationResult { - val path = request.getProxyAwarePath() - - if (path == "/" || WHITELISTED_PATH_PREFIXES.any { path.startsWith(it) }) { - return AuthorizationResult.success() - } - - val accessKey = request.getStringField(ACCESS_KEY_PROPERTY) - ?: return AuthorizationResult.failure("An access key is required to access $path.") - - if (accessKeys.fullAccessKeys.contains(accessKey)) { - return AuthorizationResult.success() - } - - if (getCurrentAccessKeys(accessKeys.aggregatedDataAccessKeys).contains(accessKey) && - endpointServesAggregatedData(request) - ) { - return AuthorizationResult.success() - } - - return AuthorizationResult.failure("You are not authorized to access $path.") - } - - private fun endpointServesAggregatedData(request: CachedBodyHttpServletRequest): Boolean { - val fields = request.getStringArrayField(FIELDS_PROPERTY) - if (containsOnlyPrimaryKey(fields)) { - return true - } - - if (!ENDPOINTS_THAT_SERVE_AGGREGATED_DATA.contains(request.getProxyAwarePath())) { - return false - } - - if (fieldsThatServeNonAggregatedData.intersect(request.getRequestFieldNames()).isNotEmpty()) { - return false - } - - return fields.intersect(fieldsThatServeNonAggregatedData.toSet()).isEmpty() - } - - private fun containsOnlyPrimaryKey(fields: List) = - fields.size == 1 && fields.first() == databaseConfig.schema.primaryKey - - private fun hash(text: String): String { - val bytes = text.toByteArray() - val md = MessageDigest.getInstance("SHA-256") - val digest = md.digest(bytes) - return digest.fold("", { str, it -> str + "%02x".format(it) }) - } - - private fun getCurrentAccessKeys(baseKey: String): Set { - val current = Instant.now().epochSecond - return listOf(-3, -2, -1, 0, 1, 2, 3) - .map { current + it } - .map { hash("$baseKey:$it") } - .toSet() - } - - private fun getCurrentAccessKeys(baseKeys: List): Set = - baseKeys.flatMap { - getCurrentAccessKeys(it) - }.toSet() -} diff --git a/lapis/src/main/kotlin/org/genspectrum/lapis/config/AccessKeys.kt b/lapis/src/main/kotlin/org/genspectrum/lapis/config/AccessKeys.kt deleted file mode 100644 index 6da03de02..000000000 --- a/lapis/src/main/kotlin/org/genspectrum/lapis/config/AccessKeys.kt +++ /dev/null @@ -1,26 +0,0 @@ -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( - @param: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 fullAccessKeys: List, - val aggregatedDataAccessKeys: List, -) diff --git a/lapis/src/main/kotlin/org/genspectrum/lapis/config/DatabaseConfig.kt b/lapis/src/main/kotlin/org/genspectrum/lapis/config/DatabaseConfig.kt index 2bd9a3933..f051780c5 100644 --- a/lapis/src/main/kotlin/org/genspectrum/lapis/config/DatabaseConfig.kt +++ b/lapis/src/main/kotlin/org/genspectrum/lapis/config/DatabaseConfig.kt @@ -57,16 +57,4 @@ enum class OpennessLevel { * The data served by this instance is fully open and may be shared with anyone. */ OPEN, - - /** - * The data served by this instance must not be disclosed to everyone. - * - * Two access keys can be configured: - * - * One access key permits access to aggregated data. The aggregated data must be such that one cannot deduce - * information about the data of the individual sequences. - * - * The other access key permits access to the full data. - */ - PROTECTED, } diff --git a/lapis/src/main/kotlin/org/genspectrum/lapis/model/mutationsOverTime/MutationsOverTimeModel.kt b/lapis/src/main/kotlin/org/genspectrum/lapis/model/mutationsOverTime/MutationsOverTimeModel.kt index 6b4b9e1d4..0cd6492ad 100644 --- a/lapis/src/main/kotlin/org/genspectrum/lapis/model/mutationsOverTime/MutationsOverTimeModel.kt +++ b/lapis/src/main/kotlin/org/genspectrum/lapis/model/mutationsOverTime/MutationsOverTimeModel.kt @@ -213,8 +213,8 @@ class MutationsOverTimeModel( val tasks = mutations.map { mutation -> Callable { - val counts = sendQuery(baseFilter, dateQuery, countQueryFn(mutation), dateField, false) - val coverage = sendQuery(baseFilter, dateQuery, coverageQueryFn(mutation), dateField, false) + val counts = sendQuery(baseFilter, dateQuery, countQueryFn(mutation), dateField) + val coverage = sendQuery(baseFilter, dateQuery, coverageQueryFn(mutation), dateField) listOf(counts.dataVersion, coverage.dataVersion) to aggregateDailyMutationDataIntoDateRanges( counts.queryResult, @@ -267,7 +267,6 @@ class MutationsOverTimeModel( dateQuery: SiloFilterExpression, mutationQuery: SiloFilterExpression?, dateField: String, - checkProtection: Boolean = true, ): WithDataVersion> = siloClient.sendQueryAndGetDataVersion( SiloQuery( @@ -286,7 +285,6 @@ class MutationsOverTimeModel( ), ), setRequestDataVersion = false, - checkProtection, ).map { it.toList() } /** diff --git a/lapis/src/main/kotlin/org/genspectrum/lapis/openApi/AccessKeyParameterCustomizer.kt b/lapis/src/main/kotlin/org/genspectrum/lapis/openApi/AccessKeyParameterCustomizer.kt deleted file mode 100644 index a65793313..000000000 --- a/lapis/src/main/kotlin/org/genspectrum/lapis/openApi/AccessKeyParameterCustomizer.kt +++ /dev/null @@ -1,41 +0,0 @@ -package org.genspectrum.lapis.openApi - -import io.swagger.v3.oas.models.OpenAPI -import io.swagger.v3.oas.models.parameters.Parameter -import org.genspectrum.lapis.config.DatabaseConfig -import org.genspectrum.lapis.config.OpennessLevel -import org.genspectrum.lapis.controller.INFO_ROUTE -import org.genspectrum.lapis.request.ACCESS_KEY_PROPERTY -import org.springdoc.core.customizers.OpenApiCustomizer -import org.springframework.stereotype.Component - -@Component -class AccessKeyParameterCustomizer( - private val databaseConfig: DatabaseConfig, -) : OpenApiCustomizer { - companion object { - private val PATH_WITH_ACCESS_KEY_PARAMETER = listOf( - "/sample$INFO_ROUTE", - ) - } - - override fun customise(openApi: OpenAPI) { - if (databaseConfig.schema.opennessLevel == OpennessLevel.OPEN) { - return - } - - for ((_, path) in openApi.paths.filter { (url, _) -> - PATH_WITH_ACCESS_KEY_PARAMETER.any { - url.startsWith(it) - } - }) { - path.get.addParametersItem( - Parameter() - .`in`("query") - .name(ACCESS_KEY_PROPERTY) - .description(ACCESS_KEY_DESCRIPTION) - .schema(accessKeySchema()), - ) - } - } -} diff --git a/lapis/src/main/kotlin/org/genspectrum/lapis/openApi/OpenApiDocs.kt b/lapis/src/main/kotlin/org/genspectrum/lapis/openApi/OpenApiDocs.kt index 0946338f6..383553e8f 100644 --- a/lapis/src/main/kotlin/org/genspectrum/lapis/openApi/OpenApiDocs.kt +++ b/lapis/src/main/kotlin/org/genspectrum/lapis/openApi/OpenApiDocs.kt @@ -13,7 +13,6 @@ import org.genspectrum.lapis.config.DatabaseConfig import org.genspectrum.lapis.config.DatabaseMetadata import org.genspectrum.lapis.config.DatabaseSchema import org.genspectrum.lapis.config.MetadataType -import org.genspectrum.lapis.config.OpennessLevel import org.genspectrum.lapis.config.ReferenceGenomeSchema import org.genspectrum.lapis.config.ReferenceSequenceSchema import org.genspectrum.lapis.config.SequenceFilterFieldName @@ -36,7 +35,6 @@ import org.genspectrum.lapis.controller.middleware.Compression import org.genspectrum.lapis.controller.middleware.DataFormat import org.genspectrum.lapis.controller.middleware.SequencesDataFormat import org.genspectrum.lapis.controller.middleware.TreeDataFormat -import org.genspectrum.lapis.request.ACCESS_KEY_PROPERTY import org.genspectrum.lapis.request.AMINO_ACID_INSERTIONS_PROPERTY import org.genspectrum.lapis.request.AMINO_ACID_MUTATIONS_PROPERTY import org.genspectrum.lapis.request.AminoAcidInsertion @@ -78,7 +76,7 @@ fun buildOpenApiSchema( Schema() .types(setOf("object")) .description("valid filters for sequence data") - .properties(computePrimitiveFieldFilters(databaseConfig, sequenceFilterFields)), + .properties(computePrimitiveFieldFilters(sequenceFilterFields)), ) .addSchemas( REQUEST_SCHEMA_WITH_MIN_PROPORTION, @@ -87,7 +85,6 @@ fun buildOpenApiSchema( .description("valid filters for sequence data") .properties( getSequenceFiltersWithFormat( - databaseConfig = databaseConfig, sequenceFilterFields = sequenceFilterFields, orderByFieldsSchema = mutationsOrderByFieldsEnum(), dataFormatSchema = dataFormatSchema(), @@ -99,7 +96,6 @@ fun buildOpenApiSchema( AGGREGATED_REQUEST_SCHEMA, requestSchemaWithFields( getSequenceFiltersWithFormat( - databaseConfig = databaseConfig, sequenceFilterFields = sequenceFilterFields, orderByFieldsSchema = aggregatedOrderByFieldsEnum(databaseConfig), dataFormatSchema = dataFormatSchema(), @@ -112,7 +108,6 @@ fun buildOpenApiSchema( DETAILS_REQUEST_SCHEMA, requestSchemaWithFields( getSequenceFiltersWithFormat( - databaseConfig = databaseConfig, sequenceFilterFields = sequenceFilterFields, orderByFieldsSchema = detailsOrderByFieldsEnum(databaseConfig), dataFormatSchema = dataFormatSchema(), @@ -125,7 +120,6 @@ fun buildOpenApiSchema( MOST_RECENT_COMMON_ANCESTOR_REQUEST_SCHEMA, requestSchemaPhyloTree( getSequenceFiltersWithFormat( - databaseConfig = databaseConfig, sequenceFilterFields = sequenceFilterFields, orderByFieldsSchema = detailsOrderByFieldsEnum(databaseConfig), dataFormatSchema = dataFormatSchema(), @@ -136,7 +130,6 @@ fun buildOpenApiSchema( PHYLO_SUBTREE_REQUEST_SCHEMA, requestSchemaPhyloTree( getSequenceFiltersWithFormat( - databaseConfig = databaseConfig, sequenceFilterFields = sequenceFilterFields, orderByFieldsSchema = detailsOrderByFieldsEnum(databaseConfig), dataFormatSchema = treeFormatSchema(), @@ -147,7 +140,6 @@ fun buildOpenApiSchema( INSERTIONS_REQUEST_SCHEMA, requestSchemaForCommonSequenceFilters( getSequenceFiltersWithFormat( - databaseConfig = databaseConfig, sequenceFilterFields = sequenceFilterFields, orderByFieldsSchema = insertionsOrderByFieldsEnum(), dataFormatSchema = dataFormatSchema(), @@ -158,7 +150,6 @@ fun buildOpenApiSchema( ALIGNED_AMINO_ACID_SEQUENCE_REQUEST_SCHEMA, aminoAcidSequencesRequestSchema( getSequenceFiltersWithFormat( - databaseConfig = databaseConfig, sequenceFilterFields = sequenceFilterFields, orderByFieldsSchema = aminoAcidSequenceOrderByFieldsEnum(databaseConfig), dataFormatSchema = sequencesFormatSchema(), @@ -170,7 +161,6 @@ fun buildOpenApiSchema( ALL_ALIGNED_AMINO_ACID_SEQUENCE_REQUEST_SCHEMA, requestSchemaWithGenes( requestProperties = getSequenceFiltersWithFormat( - databaseConfig = databaseConfig, sequenceFilterFields = sequenceFilterFields, orderByFieldsSchema = aminoAcidSequenceOrderByFieldsEnum(databaseConfig), dataFormatSchema = sequencesFormatSchema(), @@ -183,7 +173,6 @@ fun buildOpenApiSchema( NUCLEOTIDE_SEQUENCE_REQUEST_SCHEMA, nucleotideSequencesRequestSchema( getSequenceFiltersWithFormat( - databaseConfig = databaseConfig, sequenceFilterFields = sequenceFilterFields, orderByFieldsSchema = nucleotideSequenceOrderByFieldsEnum(databaseConfig), dataFormatSchema = sequencesFormatSchema(), @@ -195,7 +184,6 @@ fun buildOpenApiSchema( ALL_NUCLEOTIDE_SEQUENCE_REQUEST_SCHEMA, requestSchemaWithSegment( requestProperties = getSequenceFiltersWithFormat( - databaseConfig = databaseConfig, sequenceFilterFields = sequenceFilterFields, orderByFieldsSchema = nucleotideSequenceOrderByFieldsEnum(databaseConfig), dataFormatSchema = sequencesFormatSchema(), @@ -206,7 +194,7 @@ fun buildOpenApiSchema( ) .addSchemas( MUTATIONS_OVER_TIME_REQUEST_SCHEMA, - requestSchemaForMutationsOverTime(databaseConfig, sequenceFilterFields), + requestSchemaForMutationsOverTime(sequenceFilterFields), ) .addSchemas( AGGREGATED_RESPONSE_SCHEMA, @@ -377,30 +365,27 @@ fun buildOpenApiSchema( ) private fun getSequenceFiltersWithFormat( - databaseConfig: DatabaseConfig, sequenceFilterFields: SequenceFilterFields, orderByFieldsSchema: Schema, dataFormatSchema: Schema<*>, ): Map> = - getSequenceFilters(databaseConfig, sequenceFilterFields, orderByFieldsSchema) + + getSequenceFilters(sequenceFilterFields, orderByFieldsSchema) + Pair(FORMAT_PROPERTY, dataFormatSchema) private fun getSequenceFilters( - databaseConfig: DatabaseConfig, sequenceFilterFields: SequenceFilterFields, orderByFieldsSchema: Schema<*>, ): Map> = - getBaseSequenceFilters(databaseConfig, sequenceFilterFields) + + getBaseSequenceFilters(sequenceFilterFields) + Pair(ORDER_BY_PROPERTY, orderByPostSchema(orderByFieldsSchema)) + Pair(LIMIT_PROPERTY, limitSchema()) + Pair(OFFSET_PROPERTY, offsetSchema()) + getCompressionAndDownloadSchema() private fun getBaseSequenceFilters( - databaseConfig: DatabaseConfig, sequenceFilterFields: SequenceFilterFields, ): Map> = - computePrimitiveFieldFilters(databaseConfig, sequenceFilterFields) + + computePrimitiveFieldFilters(sequenceFilterFields) + Pair(NUCLEOTIDE_MUTATIONS_PROPERTY, nucleotideMutations()) + Pair(AMINO_ACID_MUTATIONS_PROPERTY, aminoAcidMutations()) + Pair(NUCLEOTIDE_INSERTIONS_PROPERTY, nucleotideInsertions()) + @@ -428,15 +413,8 @@ fun compressionSchema(): Schema<*> = ._enum(Compression.entries.map { it.value }) private fun computePrimitiveFieldFilters( - databaseConfig: DatabaseConfig, sequenceFilterFields: SequenceFilterFields, -): Map> = - when (databaseConfig.schema.opennessLevel) { - OpennessLevel.PROTECTED -> primitiveSequenceFilterFieldSchemas(sequenceFilterFields) + - (ACCESS_KEY_PROPERTY to accessKeySchema()) - - else -> primitiveSequenceFilterFieldSchemas(sequenceFilterFields) - } +): Map> = primitiveSequenceFilterFieldSchemas(sequenceFilterFields) private fun lapisResponseSchema(dataSchema: Schema) = Schema().types(setOf("object")) @@ -664,10 +642,7 @@ private fun requestSchemaWithGenes( Pair(FASTA_HEADER_TEMPLATE_PROPERTY, aminoAcidFastaHeaderTemplateSchema(databaseConfig)), ) -private fun requestSchemaForMutationsOverTime( - databaseConfig: DatabaseConfig, - sequenceFilterFields: SequenceFilterFields, -): Schema<*> = +private fun requestSchemaForMutationsOverTime(sequenceFilterFields: SequenceFilterFields): Schema<*> = Schema() .types(setOf("object")) .description("Request schema for fetching mutations over time.") @@ -677,9 +652,7 @@ private fun requestSchemaForMutationsOverTime( Schema() .types(setOf("object")) .description("Sequence filters") - .properties( - getBaseSequenceFilters(databaseConfig, sequenceFilterFields), - ), + .properties(getBaseSequenceFilters(sequenceFilterFields)), ) + getCompressionAndDownloadSchema() + mapOf( @@ -714,8 +687,6 @@ private fun getAggregatedResponseProperties(filterProperties: Map = StringSchema().description(ACCESS_KEY_DESCRIPTION) - private fun nucleotideMutationProportionSchema() = mapOf( "mutation" to StringSchema() diff --git a/lapis/src/main/kotlin/org/genspectrum/lapis/openApi/Schemas.kt b/lapis/src/main/kotlin/org/genspectrum/lapis/openApi/Schemas.kt index fe07d12ae..b62fad974 100644 --- a/lapis/src/main/kotlin/org/genspectrum/lapis/openApi/Schemas.kt +++ b/lapis/src/main/kotlin/org/genspectrum/lapis/openApi/Schemas.kt @@ -136,13 +136,6 @@ Optionally set this to return the response compressed in the specified format. Alternatively, you can set the '$ACCEPT_ENCODING' header to the respective value. """ -const val ACCESS_KEY_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. -""" - const val SEGMENTS_DESCRIPTION = "List of segments to retrieve sequences for. If not provided, all segments will be returned." diff --git a/lapis/src/main/kotlin/org/genspectrum/lapis/request/SpecialProperties.kt b/lapis/src/main/kotlin/org/genspectrum/lapis/request/SpecialProperties.kt index 5174b9d26..b72b99aad 100644 --- a/lapis/src/main/kotlin/org/genspectrum/lapis/request/SpecialProperties.kt +++ b/lapis/src/main/kotlin/org/genspectrum/lapis/request/SpecialProperties.kt @@ -3,7 +3,6 @@ package org.genspectrum.lapis.request import com.fasterxml.jackson.databind.node.JsonNodeType const val FORMAT_PROPERTY = "dataFormat" -const val ACCESS_KEY_PROPERTY = "accessKey" const val MIN_PROPORTION_PROPERTY = "minProportion" const val FIELDS_PROPERTY = "fields" const val NUCLEOTIDE_MUTATIONS_PROPERTY = "nucleotideMutations" @@ -24,7 +23,6 @@ const val PHYLO_TREE_FIELD_PROPERTY = "phyloTreeField" val SPECIAL_REQUEST_PROPERTY_TYPES = mapOf( FORMAT_PROPERTY to JsonNodeType.STRING, - ACCESS_KEY_PROPERTY to JsonNodeType.STRING, MIN_PROPORTION_PROPERTY to JsonNodeType.NUMBER, FIELDS_PROPERTY to JsonNodeType.ARRAY, NUCLEOTIDE_MUTATIONS_PROPERTY to JsonNodeType.ARRAY, diff --git a/lapis/src/main/kotlin/org/genspectrum/lapis/silo/SiloClient.kt b/lapis/src/main/kotlin/org/genspectrum/lapis/silo/SiloClient.kt index f2077f6ce..5193e39b1 100644 --- a/lapis/src/main/kotlin/org/genspectrum/lapis/silo/SiloClient.kt +++ b/lapis/src/main/kotlin/org/genspectrum/lapis/silo/SiloClient.kt @@ -4,14 +4,11 @@ import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import org.genspectrum.lapis.config.DatabaseConfig -import org.genspectrum.lapis.config.OpennessLevel -import org.genspectrum.lapis.controller.BadRequestException import org.genspectrum.lapis.controller.LapisHeaders.REQUEST_ID import org.genspectrum.lapis.log import org.genspectrum.lapis.logging.RequestContext import org.genspectrum.lapis.logging.RequestIdContext import org.genspectrum.lapis.response.InfoData -import org.genspectrum.lapis.silo.SiloAction.AggregatedAction import org.genspectrum.lapis.util.YamlObjectMapper import org.springframework.cache.annotation.Cacheable import org.springframework.http.HttpHeaders @@ -43,11 +40,10 @@ class SiloClient( fun sendQueryAndGetDataVersion( query: SiloQuery, setRequestDataVersion: Boolean = true, - checkProtection: Boolean = true, ): WithDataVersion> { val response = when (query.action.cacheable) { - true -> cachedSiloClient.sendCachedQuery(query, checkProtection).map { it.stream() } - else -> cachedSiloClient.sendQuery(query, checkProtection) + true -> cachedSiloClient.sendCachedQuery(query).map { it.stream() } + else -> cachedSiloClient.sendQuery(query) } if (setRequestDataVersion) { @@ -104,35 +100,11 @@ open class CachedSiloClient( "#query.action.randomize.class.simpleName == 'Disabled' || " + "#query.action.randomize.class.simpleName == 'WithSeed')", ) - open fun sendCachedQuery( - query: SiloQuery, - checkProtection: Boolean = true, - ): WithDataVersion> = - sendQuery(query, checkProtection) + open fun sendCachedQuery(query: SiloQuery): WithDataVersion> = + sendQuery(query) .let { WithDataVersion(it.dataVersion, it.queryResult.toList()) } - fun sendQuery( - query: SiloQuery, - checkProtection: Boolean = true, - ): WithDataVersion> { - // If the data is protected: - // 1. If it is an aggregated request asking for more than 3 fields, throw an error. - // 2. Check whether the number of sequences matching the filter set. If it is more than 0 but less than 10, - // throw an error. (Exception: it is an aggregated request without any fields.) - if (checkProtection && config.schema.opennessLevel == OpennessLevel.PROTECTED) { - if (query.action is AggregatedAction && query.action.groupByFields.size > 3) { - throw BadRequestException("Unauthorized") - } - if (!(query.action is AggregatedAction && query.action.groupByFields.isEmpty())) { - val countQuery = SiloQuery(SiloAction.aggregated(), query.filterExpression) - val countResponseStream = sendQuery(countQuery, false) - val countResponse = countResponseStream.queryResult.toList().first() - if (countResponse.count in 1..<10) { - throw BadRequestException("Unauthorized") - } - } - } - + fun sendQuery(query: SiloQuery): WithDataVersion> { if (RequestContextHolder.getRequestAttributes() != null) { requestContext.cached = false } diff --git a/lapis/src/main/resources/application-docker.properties b/lapis/src/main/resources/application-docker.properties index c3e19fc43..395b53cc7 100644 --- a/lapis/src/main/resources/application-docker.properties +++ b/lapis/src/main/resources/application-docker.properties @@ -1,2 +1 @@ lapis.databaseConfig.path=./database_config.yaml -lapis.accessKeys.path=./access_keys.yaml diff --git a/lapis/src/test/kotlin/org/genspectrum/lapis/auth/ProtectedDataAuthorizationTest.kt b/lapis/src/test/kotlin/org/genspectrum/lapis/auth/ProtectedDataAuthorizationTest.kt deleted file mode 100644 index 437d2ad5b..000000000 --- a/lapis/src/test/kotlin/org/genspectrum/lapis/auth/ProtectedDataAuthorizationTest.kt +++ /dev/null @@ -1,367 +0,0 @@ -package org.genspectrum.lapis.auth - -import com.ninjasquad.springmockk.MockkBean -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.verify -import org.genspectrum.lapis.PRIMARY_KEY_FIELD -import org.genspectrum.lapis.controller.AGGREGATED_ROUTE -import org.genspectrum.lapis.controller.DATABASE_CONFIG_ROUTE -import org.genspectrum.lapis.controller.DETAILS_ROUTE -import org.genspectrum.lapis.controller.REFERENCE_GENOME_ROUTE -import org.genspectrum.lapis.controller.getSample -import org.genspectrum.lapis.controller.postSample -import org.genspectrum.lapis.model.SiloQueryModel -import org.genspectrum.lapis.request.SequenceFiltersRequestWithFields -import org.genspectrum.lapis.response.LapisInfo -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.ValueSource -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.get -import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content -import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status -import java.security.MessageDigest -import java.time.Instant -import java.util.stream.Stream - -private const val NOT_AUTHORIZED_TO_ACCESS_ENDPOINT_ERROR = """ -{ - "error" : { - "title": "Forbidden", - "detail": "You are not authorized to access /sample/aggregated." - } -} -""" - -private const val FORBIDDEN_TO_ACCESS_ENDPOINT_ERROR = """ -{ - "error" : { - "title": "Forbidden", - "detail": "An access key is required to access /sample/aggregated." - } -} -""" - -@SpringBootTest(properties = ["lapis.databaseConfig.path=src/test/resources/config/protectedDataDatabaseConfig.yaml"]) -@AutoConfigureMockMvc -class ProtectedDataAuthorizationTest( - @param:Autowired val mockMvc: MockMvc, -) { - @MockkBean - lateinit var siloQueryModelMock: SiloQueryModel - - private val validRoute = AGGREGATED_ROUTE - - @MockkBean - lateinit var lapisInfo: LapisInfo - - @BeforeEach - fun setUp() { - every { siloQueryModelMock.getAggregated(any()) } returns Stream.empty() - - every { - lapisInfo.dataVersion - } returns "1234" - - MockKAnnotations.init(this) - } - - @Test - fun `given no access key in GET request to protected instance, then access is denied`() { - mockMvc.perform(getSample(validRoute)) - .andExpect(status().isForbidden) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(FORBIDDEN_TO_ACCESS_ENDPOINT_ERROR)) - } - - @Test - fun `given no access key in POST request to protected instance, then access is denied`() { - mockMvc.perform(postRequestWithBody("")) - .andExpect(status().isForbidden) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(FORBIDDEN_TO_ACCESS_ENDPOINT_ERROR)) - } - - @Test - fun `given wrong access key in GET request to protected instance, then access is denied`() { - mockMvc.perform(getSample("$validRoute?accessKey=invalidKey")) - .andExpect(status().isForbidden) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(NOT_AUTHORIZED_TO_ACCESS_ENDPOINT_ERROR)) - } - - @Test - fun `given too old access key in GET request to protected instance, then access is denied`() { - val oldKey = getCurrentAccessKey("testAggregatedDataAccessKey", Instant.now().epochSecond - 10) - mockMvc.perform(getSample("$validRoute?accessKey=$oldKey")) - .andExpect(status().isForbidden) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(NOT_AUTHORIZED_TO_ACCESS_ENDPOINT_ERROR)) - } - - @Test - fun `given wrong access key in POST request to protected instance, then access is denied`() { - mockMvc.perform(postRequestWithBody("""{"accessKey": "invalidKey"}""")) - .andExpect(status().isForbidden) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(NOT_AUTHORIZED_TO_ACCESS_ENDPOINT_ERROR)) - } - - @Test - fun `given valid access key for aggregated data in GET request to protected instance, then access is granted`() { - val currentKey = getCurrentAccessKey("testAggregatedDataAccessKey") - mockMvc.perform( - getSample("$validRoute?accessKey=$currentKey&field1=value1"), - ) - .andExpect(status().isOk) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - - verify { siloQueryModelMock.getAggregated(sequenceFilterRequest()) } - } - - @Test - fun `given 2s old access key for aggregated data in GET request to protected instance, then access is granted`() { - val currentKey = getCurrentAccessKey("testAggregatedDataAccessKey", Instant.now().epochSecond - 2) - mockMvc.perform( - getSample("$validRoute?accessKey=$currentKey&field1=value1"), - ) - .andExpect(status().isOk) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - - verify { siloQueryModelMock.getAggregated(sequenceFilterRequest()) } - } - - @Test - fun `given second valid access key for agg data in GET request to protected instance, then access is granted`() { - val currentKey = getCurrentAccessKey("testAggregatedDataAccessKey2") - mockMvc.perform( - getSample("$validRoute?accessKey=$currentKey&field1=value1"), - ) - .andExpect(status().isOk) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - - verify { siloQueryModelMock.getAggregated(sequenceFilterRequest()) } - } - - @Test - fun `given valid access key for aggregated data in POST request to protected instance, then access is granted`() { - val currentKey = getCurrentAccessKey("testAggregatedDataAccessKey") - mockMvc.perform( - postRequestWithBody( - """ { - "accessKey": "$currentKey", - "field1": "value1" - }""", - ), - ) - .andExpect(status().isOk) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - - verify { siloQueryModelMock.getAggregated(sequenceFilterRequest()) } - } - - @Test - fun `given aggregated access key in GET request but filters are too fine-grained, then access is denied`() { - val currentKey = getCurrentAccessKey("testAggregatedDataAccessKey") - mockMvc.perform( - getSample("$validRoute?accessKey=$currentKey&$PRIMARY_KEY_FIELD=value"), - ) - .andExpect(status().isForbidden) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(NOT_AUTHORIZED_TO_ACCESS_ENDPOINT_ERROR)) - } - - @Test - fun `given aggregated access key in POST request but filters are too fine-grained, then access is denied`() { - val currentKey = getCurrentAccessKey("testAggregatedDataAccessKey") - mockMvc.perform( - postRequestWithBody( - """ { - "accessKey": "$currentKey", - "$PRIMARY_KEY_FIELD": "some value" - }""", - ), - ) - .andExpect(status().isForbidden) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(NOT_AUTHORIZED_TO_ACCESS_ENDPOINT_ERROR)) - } - - @ParameterizedTest - @ValueSource( - strings = [ - "fields=$PRIMARY_KEY_FIELD,country", - "fields=$PRIMARY_KEY_FIELD&fields=country", - ], - ) - fun `GIVEN aggregated access key in GET request but request stratifies too fine-grained THEN access is denied`( - fieldsQueryParameter: String, - ) { - val currentKey = getCurrentAccessKey("testAggregatedDataAccessKey") - mockMvc.perform( - getSample("$validRoute?accessKey=$currentKey&$fieldsQueryParameter"), - ) - .andExpect(status().isForbidden) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(NOT_AUTHORIZED_TO_ACCESS_ENDPOINT_ERROR)) - } - - @Test - fun `GIVEN aggregated access key in GET request that stratifies non-fine-grained THEN access is granted`() { - val currentKey = getCurrentAccessKey("testAggregatedDataAccessKey") - mockMvc.perform( - getSample("$validRoute?accessKey=$currentKey&fields=country"), - ) - .andExpect(status().isOk) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - } - - @Test - fun `GIVEN aggregated access key in POST request but request stratifies too fine-grained THEN access is denied`() { - val currentKey = getCurrentAccessKey("testAggregatedDataAccessKey") - mockMvc.perform( - postRequestWithBody( - """ { - "accessKey": "$currentKey", - "fields": ["$PRIMARY_KEY_FIELD", "country"] - }""", - ), - ) - .andExpect(status().isForbidden) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(NOT_AUTHORIZED_TO_ACCESS_ENDPOINT_ERROR)) - } - - @Test - fun `GIVEN aggregated access key in GET request where fields only contains primary key THEN access is granted`() { - val currentKey = getCurrentAccessKey("testAggregatedDataAccessKey") - mockMvc.perform( - getSample("$validRoute?accessKey=$currentKey&fields=$PRIMARY_KEY_FIELD"), - ) - .andExpect(status().isOk) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - } - - @Test - fun `GIVEN aggregated access key in POST request where fields only contains primary key THEN access is granted`() { - val currentKey = getCurrentAccessKey("testAggregatedDataAccessKey") - mockMvc.perform( - postRequestWithBody( - """ { - "accessKey": "$currentKey", - "fields": ["$PRIMARY_KEY_FIELD"] - }""", - ), - ) - .andExpect(status().isOk) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - } - - @Test - fun `GIVEN aggregated accessKey in details request where fields only contains primaryKey THEN access is granted`() { - every { siloQueryModelMock.getDetails(any()) } returns Stream.empty() - - val currentKey = getCurrentAccessKey("testAggregatedDataAccessKey") - mockMvc.perform( - postSample(DETAILS_ROUTE) - .contentType(MediaType.APPLICATION_JSON) - .content( - """{ - "accessKey": "$currentKey", - "fields": ["$PRIMARY_KEY_FIELD"] - }""", - ), - ) - .andExpect(status().isOk) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - } - - @Test - fun `given valid access key for full access in GET request to protected instance, then access is granted`() { - mockMvc.perform( - getSample("$validRoute?accessKey=testFullAccessKey&field1=value1"), - ) - .andExpect(status().isOk) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - - verify { siloQueryModelMock.getAggregated(sequenceFilterRequest()) } - } - - @Test - fun `given second valid access key for full access in GET request to protected instance, then access is granted`() { - mockMvc.perform( - getSample("$validRoute?accessKey=testFullAccessKey2&field1=value1"), - ) - .andExpect(status().isOk) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - - verify { siloQueryModelMock.getAggregated(sequenceFilterRequest()) } - } - - @Test - fun `given valid access key for full access in POST request to protected instance, then access is granted`() { - mockMvc.perform( - postRequestWithBody( - """ { - "accessKey": "testFullAccessKey", - "field1": "value1" - }""", - ), - ) - .andExpect(status().isOk) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - - verify { siloQueryModelMock.getAggregated(sequenceFilterRequest()) } - } - - private fun sequenceFilterRequest() = - SequenceFiltersRequestWithFields( - mapOf("field1" to listOf("value1")), - emptyList(), - emptyList(), - emptyList(), - emptyList(), - emptyList(), - ) - - @Test - fun `whitelisted routes are always accessible`() { - mockMvc.perform(get("/swagger-ui/index.html")) - .andExpect(status().isOk) - - mockMvc.perform(get("/api-docs")) - .andExpect(status().isOk) - - mockMvc.perform(get("/api-docs.yaml")) - .andExpect(status().isOk) - - mockMvc.perform(getSample(DATABASE_CONFIG_ROUTE)) - .andExpect(status().isOk) - - mockMvc.perform(getSample(REFERENCE_GENOME_ROUTE)) - .andExpect(status().isOk) - } - - private fun postRequestWithBody(body: String) = - postSample(validRoute) - .contentType(MediaType.APPLICATION_JSON) - .content(body) - - private fun hash(text: String): String { - val bytes = text.toByteArray() - val md = MessageDigest.getInstance("SHA-256") - val digest = md.digest(bytes) - return digest.fold("", { str, it -> str + "%02x".format(it) }) - } - - private fun getCurrentAccessKey( - baseKey: String, - epoch: Long = Instant.now().epochSecond, - ): String = hash("$baseKey:$epoch") -} diff --git a/lapis/src/test/kotlin/org/genspectrum/lapis/config/AccessKeysReaderTest.kt b/lapis/src/test/kotlin/org/genspectrum/lapis/config/AccessKeysReaderTest.kt deleted file mode 100644 index 2c5765ec3..000000000 --- a/lapis/src/test/kotlin/org/genspectrum/lapis/config/AccessKeysReaderTest.kt +++ /dev/null @@ -1,48 +0,0 @@ -package org.genspectrum.lapis.config - -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.contains -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.fullAccessKeys, - contains( - "testFullAccessKey", - "testFullAccessKey2", - ), - ) - - assertThat( - result.aggregatedDataAccessKeys, - contains( - "testAggregatedDataAccessKey", - "testAggregatedDataAccessKey2", - ), - ) - } -} - -@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/lapis/src/test/kotlin/org/genspectrum/lapis/model/mutationsOverTime/AminoAcidMutationsOverTimeModelTest.kt b/lapis/src/test/kotlin/org/genspectrum/lapis/model/mutationsOverTime/AminoAcidMutationsOverTimeModelTest.kt index 15638f3c3..f889d23af 100644 --- a/lapis/src/test/kotlin/org/genspectrum/lapis/model/mutationsOverTime/AminoAcidMutationsOverTimeModelTest.kt +++ b/lapis/src/test/kotlin/org/genspectrum/lapis/model/mutationsOverTime/AminoAcidMutationsOverTimeModelTest.kt @@ -231,7 +231,7 @@ class AminoAcidMutationsOverTimeModelTest { fun `given one data version change, then it succeeds`() { var callCount = 0 every { - siloQueryClient.sendQueryAndGetDataVersion(any(), false, any()) + siloQueryClient.sendQueryAndGetDataVersion(any(), false) } answers { val version = if (callCount++ == 0) "1" else "2" WithDataVersion( @@ -259,7 +259,7 @@ class AminoAcidMutationsOverTimeModelTest { fun `given more than once data version change, then it throws`() { var callCount = 0 every { - siloQueryClient.sendQueryAndGetDataVersion(any(), false, any()) + siloQueryClient.sendQueryAndGetDataVersion(any(), false) } answers { val version = (callCount++).toString() WithDataVersion( diff --git a/lapis/src/test/kotlin/org/genspectrum/lapis/model/mutationsOverTime/Helpers.kt b/lapis/src/test/kotlin/org/genspectrum/lapis/model/mutationsOverTime/Helpers.kt index c39908f3a..4b4eebc5c 100644 --- a/lapis/src/test/kotlin/org/genspectrum/lapis/model/mutationsOverTime/Helpers.kt +++ b/lapis/src/test/kotlin/org/genspectrum/lapis/model/mutationsOverTime/Helpers.kt @@ -79,7 +79,6 @@ fun mockSiloCountQuery( query.filterExpression.children.contains(dateBetweenFilter) }, false, - false, ) } answers { WithDataVersion(DUMMY_DATA_VERSION, queryResult) @@ -141,7 +140,6 @@ private fun mockSiloCoverageQuery( query.filterExpression.children.contains(dateBetween) }, false, - false, ) } answers { WithDataVersion(DUMMY_DATA_VERSION, queryResult) diff --git a/lapis/src/test/kotlin/org/genspectrum/lapis/model/mutationsOverTime/NucleotideMutationsOverTimeModelTest.kt b/lapis/src/test/kotlin/org/genspectrum/lapis/model/mutationsOverTime/NucleotideMutationsOverTimeModelTest.kt index b8a5e8627..b5db4b5be 100644 --- a/lapis/src/test/kotlin/org/genspectrum/lapis/model/mutationsOverTime/NucleotideMutationsOverTimeModelTest.kt +++ b/lapis/src/test/kotlin/org/genspectrum/lapis/model/mutationsOverTime/NucleotideMutationsOverTimeModelTest.kt @@ -234,7 +234,7 @@ class NucleotideMutationsOverTimeModelTest { fun `given one data version change, then it succeeds`() { var callCount = 0 every { - siloQueryClient.sendQueryAndGetDataVersion(any(), false, any()) + siloQueryClient.sendQueryAndGetDataVersion(any(), false) } answers { val version = if (callCount++ == 0) "1" else "2" WithDataVersion( @@ -262,7 +262,7 @@ class NucleotideMutationsOverTimeModelTest { fun `given more than once data version change, then it throws`() { var callCount = 0 every { - siloQueryClient.sendQueryAndGetDataVersion(any(), false, any()) + siloQueryClient.sendQueryAndGetDataVersion(any(), false) } answers { val version = (callCount++).toString() WithDataVersion( diff --git a/lapis/src/test/kotlin/org/genspectrum/lapis/request/MutationProportionsRequestTest.kt b/lapis/src/test/kotlin/org/genspectrum/lapis/request/MutationProportionsRequestTest.kt index 73aa240bb..bd0c01cdb 100644 --- a/lapis/src/test/kotlin/org/genspectrum/lapis/request/MutationProportionsRequestTest.kt +++ b/lapis/src/test/kotlin/org/genspectrum/lapis/request/MutationProportionsRequestTest.kt @@ -170,22 +170,6 @@ class MutationProportionsRequestTest { minProportion = 0.7, ), ), - Arguments.of( - """ - { - "accessKey": "some access key" - } - """, - MutationProportionsRequest( - sequenceFilters = emptyMap(), - nucleotideMutations = emptyList(), - aminoAcidMutations = emptyList(), - nucleotideInsertions = emptyList(), - aminoAcidInsertions = emptyList(), - fields = emptyList(), - minProportion = DEFAULT_MIN_PROPORTION, - ), - ), Arguments.of( "{}", MutationProportionsRequest( diff --git a/lapis/src/test/kotlin/org/genspectrum/lapis/request/SequenceFiltersRequestWithFieldsTest.kt b/lapis/src/test/kotlin/org/genspectrum/lapis/request/SequenceFiltersRequestWithFieldsTest.kt index 407250117..6fedad1bf 100644 --- a/lapis/src/test/kotlin/org/genspectrum/lapis/request/SequenceFiltersRequestWithFieldsTest.kt +++ b/lapis/src/test/kotlin/org/genspectrum/lapis/request/SequenceFiltersRequestWithFieldsTest.kt @@ -194,21 +194,6 @@ class SequenceFiltersRequestWithFieldsTest { emptyList(), ), ), - Arguments.of( - """ - { - "accessKey": "some access key" - } - """, - SequenceFiltersRequestWithFields( - emptyMap(), - emptyList(), - emptyList(), - emptyList(), - emptyList(), - emptyList(), - ), - ), Arguments.of( "{}", SequenceFiltersRequestWithFields( diff --git a/lapis/src/test/resources/application-test.properties b/lapis/src/test/resources/application-test.properties index 9a81c248e..f35aa88a0 100644 --- a/lapis/src/test/resources/application-test.properties +++ b/lapis/src/test/resources/application-test.properties @@ -1,6 +1,5 @@ silo.url=http://url.to.silo lapis.databaseConfig.path=src/test/resources/config/testDatabaseConfig.yaml -lapis.accessKeys.path=src/test/resources/config/testAccessKeys.yaml referenceGenome.segments=main,other_segment referenceGenome.genes=gene1,gene2 referenceGenomeFilename=src/test/resources/config/reference-genomes.json diff --git a/lapis/src/test/resources/config/protectedDataDatabaseConfig.yaml b/lapis/src/test/resources/config/protectedDataDatabaseConfig.yaml deleted file mode 100644 index 3851e7e3b..000000000 --- a/lapis/src/test/resources/config/protectedDataDatabaseConfig.yaml +++ /dev/null @@ -1,10 +0,0 @@ -schema: - instanceName: protectedDataTestConfig - opennessLevel: PROTECTED - metadata: - - name: primaryKey - type: string - valuesAreUnique: true - - name: country - type: string - primaryKey: primaryKey diff --git a/lapis/src/test/resources/config/testAccessKeys.yaml b/lapis/src/test/resources/config/testAccessKeys.yaml deleted file mode 100644 index 68aea6603..000000000 --- a/lapis/src/test/resources/config/testAccessKeys.yaml +++ /dev/null @@ -1,6 +0,0 @@ -fullAccessKeys: - - testFullAccessKey - - testFullAccessKey2 -aggregatedDataAccessKeys: - - testAggregatedDataAccessKey - - testAggregatedDataAccessKey2