Skip to content

Commit

Permalink
feat(backend): implement a superuser role #785
Browse files Browse the repository at this point in the history
It allows curators to do everything that a normal group member could do on behalf of the group.
  • Loading branch information
fengelniederhammer committed Mar 27, 2024
1 parent a14dcb7 commit 94973f2
Show file tree
Hide file tree
Showing 39 changed files with 970 additions and 345 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ data class SequenceEntryStatus(
override val version: Version,
val status: Status,
val group: String,
val submitter: String,
val isRevocation: Boolean = false,
val submissionId: String,
val dataUseTerms: DataUseTerms,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.loculus.backend.auth

import io.swagger.v3.oas.annotations.media.Schema
import org.loculus.backend.auth.Roles.SUPER_USER
import org.springframework.core.MethodParameter
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.oauth2.core.oidc.StandardClaimNames
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
import org.springframework.stereotype.Component
import org.springframework.web.bind.support.WebDataBinderFactory
import org.springframework.web.context.request.NativeWebRequest
import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.method.support.ModelAndViewContainer

object Roles {
const val SUPER_USER = "super_user"
const val PREPROCESSING_PIPELINE = "preprocessing_pipeline"
const val GET_RELEASED_DATA = "get_released_data"
}

class AuthenticatedUser(private val source: JwtAuthenticationToken) {
val username: String
get() = source.token.claims[StandardClaimNames.PREFERRED_USERNAME] as String

val isSuperUser: Boolean
get() = source.authorities.any { it.authority == SUPER_USER }
}

@Component
class UserConverter : HandlerMethodArgumentResolver {
override fun supportsParameter(parameter: MethodParameter): Boolean {
return AuthenticatedUser::class.java.isAssignableFrom(parameter.parameterType)
}

override fun resolveArgument(
parameter: MethodParameter,
mavContainer: ModelAndViewContainer?,
webRequest: NativeWebRequest,
binderFactory: WebDataBinderFactory?,
): Any? {
val authentication = SecurityContextHolder.getContext().authentication
if (authentication is JwtAuthenticationToken) {
return AuthenticatedUser(authentication)
}
throw IllegalArgumentException("Authentication object not of type AbstractAuthenticationToken")
}
}

/**
* Hides a parameter from the generated OpenAPI documentation.
* Usage:
*
* ```kotlin
* @RestController
* class MyController {
* @GetMapping("/my-endpoint")
* fun myFunction(@HiddenParam authenticatedUser: AuthenticatedUser) {
* // ...
* }
* }
*/
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@Schema(hidden = true)
annotation class HiddenParam
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package org.loculus.backend.config
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import mu.KotlinLogging
import org.loculus.backend.auth.Roles.GET_RELEASED_DATA
import org.loculus.backend.auth.Roles.PREPROCESSING_PIPELINE
import org.springframework.beans.factory.InitializingBean
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
Expand Down Expand Up @@ -74,8 +76,8 @@ class SecurityConfig {
).permitAll()
auth.requestMatchers(HttpMethod.GET, *getEndpointsThatArePublic).permitAll()
auth.requestMatchers(HttpMethod.OPTIONS).permitAll()
auth.requestMatchers(*endpointsForPreprocessingPipeline).hasAuthority("preprocessing_pipeline")
auth.requestMatchers(*endpointsForGettingReleasedData).hasAuthority("get_released_data")
auth.requestMatchers(*endpointsForPreprocessingPipeline).hasAuthority(PREPROCESSING_PIPELINE)
auth.requestMatchers(*endpointsForGettingReleasedData).hasAuthority(GET_RELEASED_DATA)
auth.anyRequest().authenticated()
}
.oauth2ResourceServer { oauth2 ->
Expand Down Expand Up @@ -125,9 +127,8 @@ fun getRoles(jwt: Jwt): List<String> {
}
}

val defaultRoles = emptyList<String>()
return when (realmAccess["roles"]) {
null -> defaultRoles
null -> emptyList()
is List<*> -> (realmAccess["roles"] as List<*>).filterIsInstance<String>()
else -> {
log.debug { "Ignoring value of roles in jwt because type was not List<*>" }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.loculus.backend.config

import org.loculus.backend.auth.UserConverter
import org.loculus.backend.log.OrganismMdcInterceptor
import org.springframework.context.annotation.Configuration
import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
Expand All @@ -19,4 +21,8 @@ class WebConfig : WebMvcConfigurer {
override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(OrganismMdcInterceptor())
}

override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
resolvers.add(UserConverter())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import org.loculus.backend.api.DataUseTermsChangeRequest
import org.loculus.backend.auth.AuthenticatedUser
import org.loculus.backend.auth.HiddenParam
import org.loculus.backend.service.datauseterms.DataUseTermsDatabaseService
import org.loculus.backend.utils.Accession
import org.springframework.http.HttpStatus
Expand All @@ -27,11 +29,11 @@ class DataUseTermsController(
@ResponseStatus(HttpStatus.NO_CONTENT)
@PutMapping("/data-use-terms", produces = [MediaType.APPLICATION_JSON_VALUE])
fun setNewDataUseTerms(
@UsernameFromJwt username: String,
@HiddenParam authenticatedUser: AuthenticatedUser,
@Parameter @RequestBody
request: DataUseTermsChangeRequest,
) = dataUseTermsDatabaseService.setNewDataUseTerms(
username,
authenticatedUser,
request.accessions,
request.newDataUseTerms,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import org.loculus.backend.api.Group
import org.loculus.backend.api.GroupDetails
import org.loculus.backend.auth.AuthenticatedUser
import org.loculus.backend.auth.HiddenParam
import org.loculus.backend.service.groupmanagement.GroupManagementDatabaseService
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
Expand All @@ -27,11 +29,11 @@ class GroupManagementController(
@ResponseStatus(HttpStatus.NO_CONTENT)
@PostMapping("/groups", produces = [MediaType.APPLICATION_JSON_VALUE])
fun createNewGroup(
@UsernameFromJwt username: String,
@Parameter(
description = "Information about the newly created group",
) @RequestBody group: Group,
) = groupManagementDatabaseService.createNewGroup(group, username)
@HiddenParam authenticatedUser: AuthenticatedUser,
@Parameter(description = "Information about the newly created group")
@RequestBody
group: Group,
) = groupManagementDatabaseService.createNewGroup(group, authenticatedUser)

@Operation(description = "Get details of a group.")
@ResponseStatus(HttpStatus.OK)
Expand All @@ -47,8 +49,8 @@ class GroupManagementController(
@Operation(description = "Get all groups the user is a member of.")
@ResponseStatus(HttpStatus.OK)
@GetMapping("/user/groups", produces = [MediaType.APPLICATION_JSON_VALUE])
fun getGroupsOfUser(@UsernameFromJwt username: String): List<Group> {
return groupManagementDatabaseService.getGroupsOfUser(username)
fun getGroupsOfUser(@HiddenParam authenticatedUser: AuthenticatedUser): List<Group> {
return groupManagementDatabaseService.getGroupsOfUser(authenticatedUser)
}

@Operation(description = "Get a list of all groups.")
Expand All @@ -62,25 +64,25 @@ class GroupManagementController(
@ResponseStatus(HttpStatus.NO_CONTENT)
@PutMapping("/groups/{groupName}/users/{usernameToAdd}", produces = [MediaType.APPLICATION_JSON_VALUE])
fun addUserToGroup(
@UsernameFromJwt groupMember: String,
@HiddenParam authenticatedUser: AuthenticatedUser,
@Parameter(
description = "The group name the user should be added to.",
) @PathVariable groupName: String,
@Parameter(
description = "The user name that should be added to the group.",
) @PathVariable usernameToAdd: String,
) = groupManagementDatabaseService.addUserToGroup(groupMember, groupName, usernameToAdd)
) = groupManagementDatabaseService.addUserToGroup(authenticatedUser, groupName, usernameToAdd)

@Operation(description = "Remove user from a group.")
@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping("/groups/{groupName}/users/{usernameToRemove}", produces = [MediaType.APPLICATION_JSON_VALUE])
fun removeUserFromGroup(
@UsernameFromJwt groupMember: String,
@HiddenParam authenticatedUser: AuthenticatedUser,
@Parameter(
description = "The group name the user should be removed from.",
) @PathVariable groupName: String,
@Parameter(
description = "The user name that should be removed from the group.",
) @PathVariable usernameToRemove: String,
) = groupManagementDatabaseService.removeUserFromGroup(groupMember, groupName, usernameToRemove)
) = groupManagementDatabaseService.removeUserFromGroup(authenticatedUser, groupName, usernameToRemove)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import org.loculus.backend.api.Status.APPROVED_FOR_RELEASE
import org.loculus.backend.api.SubmittedSeqSet
import org.loculus.backend.api.SubmittedSeqSetRecord
import org.loculus.backend.api.SubmittedSeqSetUpdate
import org.loculus.backend.auth.AuthenticatedUser
import org.loculus.backend.auth.HiddenParam
import org.loculus.backend.service.KeycloakAdapter
import org.loculus.backend.service.seqsetcitations.SeqSetCitationsDatabaseService
import org.loculus.backend.service.submission.SubmissionDatabaseService
Expand Down Expand Up @@ -45,15 +47,21 @@ class SeqSetCitationsController(

@Operation(description = "Create a new SeqSet with the specified data")
@PostMapping("/create-seqset")
fun createSeqSet(@UsernameFromJwt username: String, @RequestBody body: SubmittedSeqSet): ResponseSeqSet {
return seqSetCitationsService.createSeqSet(username, body.name, body.records, body.description)
fun createSeqSet(
@HiddenParam authenticatedUser: AuthenticatedUser,
@RequestBody body: SubmittedSeqSet,
): ResponseSeqSet {
return seqSetCitationsService.createSeqSet(authenticatedUser, body.name, body.records, body.description)
}

@Operation(description = "Update a SeqSet with the specified data")
@PutMapping("/update-seqset")
fun updateSeqSet(@UsernameFromJwt username: String, @RequestBody body: SubmittedSeqSetUpdate): ResponseSeqSet {
fun updateSeqSet(
@HiddenParam authenticatedUser: AuthenticatedUser,
@RequestBody body: SubmittedSeqSetUpdate,
): ResponseSeqSet {
return seqSetCitationsService.updateSeqSet(
username,
authenticatedUser,
body.seqSetId,
body.name,
body.records,
Expand All @@ -63,8 +71,8 @@ class SeqSetCitationsController(

@Operation(description = "Get a list of SeqSets created by the logged-in user")
@GetMapping("/get-seqsets-of-user")
fun getSeqSets(@UsernameFromJwt username: String): List<SeqSet> {
return seqSetCitationsService.getSeqSets(username)
fun getSeqSets(@HiddenParam authenticatedUser: AuthenticatedUser): List<SeqSet> {
return seqSetCitationsService.getSeqSets(authenticatedUser)
}

@Operation(description = "Get records for a SeqSet")
Expand All @@ -75,25 +83,29 @@ class SeqSetCitationsController(

@Operation(description = "Delete a SeqSet")
@DeleteMapping("/delete-seqset")
fun deleteSeqSet(@UsernameFromJwt username: String, @RequestParam seqSetId: String, @RequestParam version: Long) {
return seqSetCitationsService.deleteSeqSet(username, seqSetId, version)
fun deleteSeqSet(
@HiddenParam authenticatedUser: AuthenticatedUser,
@RequestParam seqSetId: String,
@RequestParam version: Long,
) {
return seqSetCitationsService.deleteSeqSet(authenticatedUser, seqSetId, version)
}

@Operation(description = "Create and associate a DOI to a SeqSet version")
@PostMapping("/create-seqset-doi")
fun createSeqSetDOI(
@UsernameFromJwt username: String,
@HiddenParam authenticatedUser: AuthenticatedUser,
@RequestParam seqSetId: String,
@RequestParam version: Long,
): ResponseSeqSet {
return seqSetCitationsService.createSeqSetDOI(username, seqSetId, version)
return seqSetCitationsService.createSeqSetDOI(authenticatedUser, seqSetId, version)
}

@Operation(description = "Get count of user sequences cited by SeqSets")
@GetMapping("/get-user-cited-by-seqset")
fun getUserCitedBySeqSet(@UsernameFromJwt username: String): CitedBy {
fun getUserCitedBySeqSet(@HiddenParam authenticatedUser: AuthenticatedUser): CitedBy {
val statusFilter = listOf(APPROVED_FOR_RELEASE)
val userSequences = submissionDatabaseService.getSequences(username, null, null, statusFilter)
val userSequences = submissionDatabaseService.getSequences(authenticatedUser, null, null, statusFilter)
return seqSetCitationsService.getUserCitedBySeqSet(userSequences.sequenceEntries)
}

Expand All @@ -107,9 +119,8 @@ class SeqSetCitationsController(
@GetMapping("/get-author")
fun getAuthor(@RequestParam username: String): AuthorProfile {
val keycloakUser = keycloakAdapter.getUsersWithName(username).firstOrNull()
if (keycloakUser == null) {
throw NotFoundException("Author profile $username does not exist")
}
?: throw NotFoundException("Author profile $username does not exist")

return seqSetCitationsService.transformKeycloakUserToAuthorProfile(keycloakUser)
}
}
Loading

0 comments on commit 94973f2

Please sign in to comment.