1
1
package org.genspectrum.lapis.auth
2
2
3
3
import com.fasterxml.jackson.databind.ObjectMapper
4
+ import com.fasterxml.jackson.module.kotlin.readValue
4
5
import jakarta.servlet.FilterChain
5
6
import jakarta.servlet.http.HttpServletRequest
6
7
import jakarta.servlet.http.HttpServletResponse
8
+ import mu.KotlinLogging
9
+ import org.genspectrum.lapis.config.AccessKeys
10
+ import org.genspectrum.lapis.config.AccessKeysReader
7
11
import org.genspectrum.lapis.config.DatabaseConfig
8
12
import org.genspectrum.lapis.config.OpennessLevel
9
13
import org.genspectrum.lapis.controller.LapisHttpErrorResponse
14
+ import org.genspectrum.lapis.util.CachedBodyHttpServletRequest
10
15
import org.springframework.http.HttpStatus
11
16
import org.springframework.http.MediaType
17
+ import org.springframework.stereotype.Component
12
18
import org.springframework.web.filter.OncePerRequestFilter
13
19
14
- abstract class DataOpennessAuthorizationFilter (val objectMapper : ObjectMapper ) : OncePerRequestFilter() {
20
+ const val ACCESS_KEY_PROPERTY = " accessKey"
21
+
22
+ private val log = KotlinLogging .logger {}
23
+
24
+ @Component
25
+ class DataOpennessAuthorizationFilterFactory (
26
+ private val databaseConfig : DatabaseConfig ,
27
+ private val objectMapper : ObjectMapper ,
28
+ private val accessKeysReader : AccessKeysReader ,
29
+ ) {
30
+ fun create () = when (databaseConfig.schema.opennessLevel) {
31
+ OpennessLevel .OPEN -> AlwaysAuthorizedAuthorizationFilter (objectMapper)
32
+ OpennessLevel .GISAID -> ProtectedGisaidDataAuthorizationFilter (
33
+ objectMapper,
34
+ accessKeysReader.read(),
35
+ databaseConfig.schema.metadata.filter { it.unique }.map { it.name },
36
+ )
37
+ }
38
+ }
39
+
40
+ abstract class DataOpennessAuthorizationFilter (protected val objectMapper : ObjectMapper ) : OncePerRequestFilter() {
15
41
override fun doFilterInternal (
16
42
request : HttpServletRequest ,
17
43
response : HttpServletResponse ,
18
44
filterChain : FilterChain ,
19
45
) {
20
- when (val result = isAuthorizedForEndpoint(request)) {
21
- AuthorizationResult .Success -> filterChain.doFilter(request, response)
46
+ val reReadableRequest = CachedBodyHttpServletRequest (request)
47
+
48
+ when (val result = isAuthorizedForEndpoint(reReadableRequest)) {
49
+ AuthorizationResult .Success -> filterChain.doFilter(reReadableRequest, response)
22
50
is AuthorizationResult .Failure -> {
23
51
response.status = HttpStatus .FORBIDDEN .value()
24
52
response.contentType = MediaType .APPLICATION_JSON_VALUE
@@ -34,15 +62,7 @@ abstract class DataOpennessAuthorizationFilter(val objectMapper: ObjectMapper) :
34
62
}
35
63
}
36
64
37
- abstract fun isAuthorizedForEndpoint (request : HttpServletRequest ): AuthorizationResult
38
-
39
- companion object {
40
- fun createFromConfig (databaseConfig : DatabaseConfig , objectMapper : ObjectMapper ) =
41
- when (databaseConfig.schema.opennessLevel) {
42
- OpennessLevel .OPEN -> NoOpAuthorizationFilter (objectMapper)
43
- OpennessLevel .GISAID -> ProtectedGisaidDataAuthorizationFilter (objectMapper)
44
- }
45
- }
65
+ abstract fun isAuthorizedForEndpoint (request : CachedBodyHttpServletRequest ): AuthorizationResult
46
66
}
47
67
48
68
sealed interface AuthorizationResult {
@@ -52,24 +72,64 @@ sealed interface AuthorizationResult {
52
72
fun failure (message : String ): AuthorizationResult = Failure (message)
53
73
}
54
74
55
- fun isSuccessful (): Boolean
56
-
57
- object Success : AuthorizationResult {
58
- override fun isSuccessful () = true
59
- }
75
+ object Success : AuthorizationResult
60
76
61
- class Failure (val message : String ) : AuthorizationResult {
62
- override fun isSuccessful () = false
63
- }
77
+ class Failure (val message : String ) : AuthorizationResult
64
78
}
65
79
66
- private class NoOpAuthorizationFilter (objectMapper : ObjectMapper ) : DataOpennessAuthorizationFilter(objectMapper) {
67
- override fun isAuthorizedForEndpoint (request : HttpServletRequest ) = AuthorizationResult .success()
80
+ private class AlwaysAuthorizedAuthorizationFilter (objectMapper : ObjectMapper ) :
81
+ DataOpennessAuthorizationFilter (objectMapper) {
82
+
83
+ override fun isAuthorizedForEndpoint (request : CachedBodyHttpServletRequest ) = AuthorizationResult .success()
68
84
}
69
85
70
- private class ProtectedGisaidDataAuthorizationFilter (objectMapper : ObjectMapper ) :
86
+ private class ProtectedGisaidDataAuthorizationFilter (
87
+ objectMapper : ObjectMapper ,
88
+ private val accessKeys : AccessKeys ,
89
+ private val fieldsThatServeNonAggregatedData : List <String >,
90
+ ) :
71
91
DataOpennessAuthorizationFilter (objectMapper) {
72
92
73
- override fun isAuthorizedForEndpoint (request : HttpServletRequest ) =
74
- AuthorizationResult .failure(" An access key is required to access this endpoint." )
93
+ companion object {
94
+ private val ENDPOINTS_THAT_SERVE_AGGREGATED_DATA = listOf (" /aggregated" , " /nucleotideMutations" )
95
+ }
96
+
97
+ override fun isAuthorizedForEndpoint (request : CachedBodyHttpServletRequest ): AuthorizationResult {
98
+ val requestFields = getRequestFields(request)
99
+
100
+ val accessKey = requestFields[ACCESS_KEY_PROPERTY ]
101
+ ? : return AuthorizationResult .failure(" An access key is required to access this endpoint." )
102
+
103
+ if (accessKeys.fullAccessKey == accessKey) {
104
+ return AuthorizationResult .success()
105
+ }
106
+
107
+ val endpointServesAggregatedData = ENDPOINTS_THAT_SERVE_AGGREGATED_DATA .contains(request.requestURI) &&
108
+ fieldsThatServeNonAggregatedData.intersect(requestFields.keys).isEmpty()
109
+
110
+ if (endpointServesAggregatedData && accessKeys.aggregatedDataAccessKey == accessKey) {
111
+ return AuthorizationResult .success()
112
+ }
113
+
114
+ return AuthorizationResult .failure(" You are not authorized to access this endpoint." )
115
+ }
116
+
117
+ private fun getRequestFields (request : CachedBodyHttpServletRequest ): Map <String , String > {
118
+ if (request.parameterNames.hasMoreElements()) {
119
+ return request.parameterMap.mapValues { (_, value) -> value.joinToString() }
120
+ }
121
+
122
+ if (request.contentLength == 0 ) {
123
+ log.warn { " Could not read access key from body, because content length is 0." }
124
+ return emptyMap()
125
+ }
126
+
127
+ return try {
128
+ objectMapper.readValue(request.inputStream)
129
+ } catch (exception: Exception ) {
130
+ log.error { " Failed to read access key from request body: ${exception.message} " }
131
+ log.debug { exception.stackTraceToString() }
132
+ emptyMap()
133
+ }
134
+ }
75
135
}
0 commit comments