diff --git a/backend/src/main/kotlin/org/loculus/backend/api/SubmissionTypes.kt b/backend/src/main/kotlin/org/loculus/backend/api/SubmissionTypes.kt index 5bb873a178..75942a3a2b 100644 --- a/backend/src/main/kotlin/org/loculus/backend/api/SubmissionTypes.kt +++ b/backend/src/main/kotlin/org/loculus/backend/api/SubmissionTypes.kt @@ -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, diff --git a/backend/src/main/kotlin/org/loculus/backend/auth/AuthenticatedUser.kt b/backend/src/main/kotlin/org/loculus/backend/auth/AuthenticatedUser.kt new file mode 100644 index 0000000000..3610add9e9 --- /dev/null +++ b/backend/src/main/kotlin/org/loculus/backend/auth/AuthenticatedUser.kt @@ -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 diff --git a/backend/src/main/kotlin/org/loculus/backend/config/SecurityConfig.kt b/backend/src/main/kotlin/org/loculus/backend/config/SecurityConfig.kt index 7b262f8ebf..4d97461a37 100644 --- a/backend/src/main/kotlin/org/loculus/backend/config/SecurityConfig.kt +++ b/backend/src/main/kotlin/org/loculus/backend/config/SecurityConfig.kt @@ -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 @@ -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 -> @@ -125,9 +127,8 @@ fun getRoles(jwt: Jwt): List { } } - val defaultRoles = emptyList() return when (realmAccess["roles"]) { - null -> defaultRoles + null -> emptyList() is List<*> -> (realmAccess["roles"] as List<*>).filterIsInstance() else -> { log.debug { "Ignoring value of roles in jwt because type was not List<*>" } diff --git a/backend/src/main/kotlin/org/loculus/backend/config/WebConfig.kt b/backend/src/main/kotlin/org/loculus/backend/config/WebConfig.kt index 1bf5da7c36..15ad28d575 100644 --- a/backend/src/main/kotlin/org/loculus/backend/config/WebConfig.kt +++ b/backend/src/main/kotlin/org/loculus/backend/config/WebConfig.kt @@ -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 @@ -19,4 +21,8 @@ class WebConfig : WebMvcConfigurer { override fun addInterceptors(registry: InterceptorRegistry) { registry.addInterceptor(OrganismMdcInterceptor()) } + + override fun addArgumentResolvers(resolvers: MutableList) { + resolvers.add(UserConverter()) + } } diff --git a/backend/src/main/kotlin/org/loculus/backend/controller/DataUseTermsController.kt b/backend/src/main/kotlin/org/loculus/backend/controller/DataUseTermsController.kt index ae525163c8..9a69615717 100644 --- a/backend/src/main/kotlin/org/loculus/backend/controller/DataUseTermsController.kt +++ b/backend/src/main/kotlin/org/loculus/backend/controller/DataUseTermsController.kt @@ -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 @@ -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, ) diff --git a/backend/src/main/kotlin/org/loculus/backend/controller/GroupManagementController.kt b/backend/src/main/kotlin/org/loculus/backend/controller/GroupManagementController.kt index 22ba5f2892..693912f6cb 100644 --- a/backend/src/main/kotlin/org/loculus/backend/controller/GroupManagementController.kt +++ b/backend/src/main/kotlin/org/loculus/backend/controller/GroupManagementController.kt @@ -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 @@ -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) @@ -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 { - return groupManagementDatabaseService.getGroupsOfUser(username) + fun getGroupsOfUser(@HiddenParam authenticatedUser: AuthenticatedUser): List { + return groupManagementDatabaseService.getGroupsOfUser(authenticatedUser) } @Operation(description = "Get a list of all groups.") @@ -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) } diff --git a/backend/src/main/kotlin/org/loculus/backend/controller/SeqSetCitationsController.kt b/backend/src/main/kotlin/org/loculus/backend/controller/SeqSetCitationsController.kt index 29ec812816..7fcf55dd3b 100644 --- a/backend/src/main/kotlin/org/loculus/backend/controller/SeqSetCitationsController.kt +++ b/backend/src/main/kotlin/org/loculus/backend/controller/SeqSetCitationsController.kt @@ -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 @@ -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, @@ -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 { - return seqSetCitationsService.getSeqSets(username) + fun getSeqSets(@HiddenParam authenticatedUser: AuthenticatedUser): List { + return seqSetCitationsService.getSeqSets(authenticatedUser) } @Operation(description = "Get records for a SeqSet") @@ -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) } @@ -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) } } diff --git a/backend/src/main/kotlin/org/loculus/backend/controller/SubmissionController.kt b/backend/src/main/kotlin/org/loculus/backend/controller/SubmissionController.kt index 5724bcd294..a7517f82cb 100644 --- a/backend/src/main/kotlin/org/loculus/backend/controller/SubmissionController.kt +++ b/backend/src/main/kotlin/org/loculus/backend/controller/SubmissionController.kt @@ -25,6 +25,8 @@ import org.loculus.backend.api.SubmissionIdMapping import org.loculus.backend.api.SubmittedProcessedData import org.loculus.backend.api.UnprocessedData import org.loculus.backend.api.WarningsFilter +import org.loculus.backend.auth.AuthenticatedUser +import org.loculus.backend.auth.HiddenParam import org.loculus.backend.model.ReleasedDataModel import org.loculus.backend.model.SubmissionParams import org.loculus.backend.model.SubmitModel @@ -35,7 +37,6 @@ import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity -import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping @@ -70,7 +71,7 @@ class SubmissionController( fun submit( @PathVariable @Valid organism: Organism, - @UsernameFromJwt username: String, + @HiddenParam authenticatedUser: AuthenticatedUser, @Parameter(description = GROUP_DESCRIPTION) @RequestParam groupName: String, @Parameter(description = METADATA_FILE_DESCRIPTION) @RequestParam metadataFile: MultipartFile, @Parameter(description = SEQUENCE_FILE_DESCRIPTION) @RequestParam sequenceFile: MultipartFile, @@ -85,7 +86,7 @@ class SubmissionController( ): List { val params = SubmissionParams.OriginalSubmissionParams( organism, - username, + authenticatedUser, metadataFile, sequenceFile, groupName, @@ -100,7 +101,7 @@ class SubmissionController( fun revise( @PathVariable @Valid organism: Organism, - @UsernameFromJwt username: String, + @HiddenParam authenticatedUser: AuthenticatedUser, @Parameter( description = REVISED_METADATA_FILE_DESCRIPTION, ) @RequestParam metadataFile: MultipartFile, @@ -110,7 +111,7 @@ class SubmissionController( ): List { val params = SubmissionParams.RevisionSubmissionParams( organism, - username, + authenticatedUser, metadataFile, sequenceFile, ) @@ -198,9 +199,9 @@ class SubmissionController( organism: Organism, @PathVariable accession: Accession, @PathVariable version: Long, - @UsernameFromJwt username: String, + @HiddenParam authenticatedUser: AuthenticatedUser, ): SequenceEntryVersionToEdit = submissionDatabaseService.getSequenceEntryVersionToEdit( - username, + authenticatedUser, AccessionVersion(accession, version), organism, ) @@ -211,9 +212,9 @@ class SubmissionController( fun submitEditedData( @PathVariable @Valid organism: Organism, - @UsernameFromJwt username: String, + @HiddenParam authenticatedUser: AuthenticatedUser, @RequestBody accessionVersion: UnprocessedData, - ) = submissionDatabaseService.submitEditedData(username, accessionVersion, organism) + ) = submissionDatabaseService.submitEditedData(authenticatedUser, accessionVersion, organism) @Operation(description = GET_SEQUENCES_DESCRIPTION) @GetMapping("/get-sequences", produces = [MediaType.APPLICATION_JSON_VALUE]) @@ -230,7 +231,7 @@ class SubmissionController( ) @RequestParam(required = false) statusesFilter: List?, - @UsernameFromJwt username: String, + @HiddenParam authenticatedUser: AuthenticatedUser, @RequestParam(required = false, defaultValue = "INCLUDE_WARNINGS") warningsFilter: WarningsFilter, @Parameter( @@ -246,7 +247,7 @@ class SubmissionController( @RequestParam(required = false) size: Int?, ): GetSequenceResponse = submissionDatabaseService.getSequences( - username, + authenticatedUser, organism, groupsFilter, statusesFilter, @@ -261,11 +262,11 @@ class SubmissionController( fun approveProcessedData( @PathVariable @Valid organism: Organism, - @UsernameFromJwt username: String, + @HiddenParam authenticatedUser: AuthenticatedUser, @RequestBody body: AccessionVersionsFilterWithApprovalScope, ): List = submissionDatabaseService.approveProcessedData( - submitter = username, + authenticatedUser = authenticatedUser, accessionVersionsFilter = body.accessionVersionsFilter, organism = organism, scope = body.scope, @@ -277,8 +278,8 @@ class SubmissionController( @PathVariable @Valid organism: Organism, @RequestBody body: Accessions, - @UsernameFromJwt username: String, - ): List = submissionDatabaseService.revoke(body.accessions, username, organism) + @HiddenParam authenticatedUser: AuthenticatedUser, + ): List = submissionDatabaseService.revoke(body.accessions, authenticatedUser, organism) @Operation(description = DELETE_SEQUENCES_DESCRIPTION) @ResponseStatus(HttpStatus.OK) @@ -288,12 +289,12 @@ class SubmissionController( fun deleteSequence( @PathVariable @Valid organism: Organism, - @UsernameFromJwt username: String, + @HiddenParam authenticatedUser: AuthenticatedUser, @RequestBody body: AccessionVersionsFilterWithDeletionScope, ): List = submissionDatabaseService.deleteSequenceEntryVersions( body.accessionVersionsFilter, - username, + authenticatedUser, organism, body.scope, ) @@ -309,8 +310,3 @@ class SubmissionController( } } } - -@Target(AnnotationTarget.VALUE_PARAMETER) -@Retention(AnnotationRetention.RUNTIME) -@AuthenticationPrincipal(expression = "claims[preferred_username]") -annotation class UsernameFromJwt diff --git a/backend/src/main/kotlin/org/loculus/backend/model/SubmitModel.kt b/backend/src/main/kotlin/org/loculus/backend/model/SubmitModel.kt index 86f9fc69e2..b5724106b7 100644 --- a/backend/src/main/kotlin/org/loculus/backend/model/SubmitModel.kt +++ b/backend/src/main/kotlin/org/loculus/backend/model/SubmitModel.kt @@ -10,6 +10,7 @@ import org.jetbrains.exposed.exceptions.ExposedSQLException import org.loculus.backend.api.DataUseTerms import org.loculus.backend.api.Organism import org.loculus.backend.api.SubmissionIdMapping +import org.loculus.backend.auth.AuthenticatedUser import org.loculus.backend.controller.BadRequestException import org.loculus.backend.controller.DuplicateKeyException import org.loculus.backend.controller.UnprocessableEntityException @@ -37,14 +38,14 @@ const val UNIQUE_CONSTRAINT_VIOLATION_SQL_STATE = "23505" interface SubmissionParams { val organism: Organism - val username: String + val authenticatedUser: AuthenticatedUser val metadataFile: MultipartFile val sequenceFile: MultipartFile val uploadType: UploadType data class OriginalSubmissionParams( override val organism: Organism, - override val username: String, + override val authenticatedUser: AuthenticatedUser, override val metadataFile: MultipartFile, override val sequenceFile: MultipartFile, val groupName: String, @@ -55,13 +56,14 @@ interface SubmissionParams { data class RevisionSubmissionParams( override val organism: Organism, - override val username: String, + override val authenticatedUser: AuthenticatedUser, override val metadataFile: MultipartFile, override val sequenceFile: MultipartFile, ) : SubmissionParams { override val uploadType: UploadType = UploadType.REVISION } } + enum class UploadType { ORIGINAL, REVISION, @@ -115,14 +117,13 @@ class SubmitModel( uploadDatabaseService.associateRevisedDataWithExistingSequenceEntries( uploadId, submissionParams.organism, - submissionParams.username, + submissionParams.authenticatedUser, ) } else if (submissionParams is SubmissionParams.OriginalSubmissionParams) { log.info { "Generating new accessions for uploaded sequence data with uploadId $uploadId" } uploadDatabaseService.generateNewAccessionsForOriginalUpload( uploadId, submissionParams.organism, - submissionParams.username, ) } @@ -135,9 +136,9 @@ class SubmitModel( private fun uploadData(uploadId: String, submissionParams: SubmissionParams, batchSize: Int) { if (submissionParams is SubmissionParams.OriginalSubmissionParams) { - groupManagementPreconditionValidator.validateUserInExistingGroup( + groupManagementPreconditionValidator.validateUserIsAllowedToModifyGroup( submissionParams.groupName, - submissionParams.username, + submissionParams.authenticatedUser, ) dataUseTermsPreconditionValidator.checkThatRestrictedUntilIsAllowed(submissionParams.dataUseTerms) } @@ -221,25 +222,26 @@ class SubmitModel( .chunked(batchSize) .forEach { batch -> uploadDatabaseService.batchInsertMetadataInAuxTable( - uploadId, - submissionParams.username, - submissionParams.groupName, - submissionParams.organism, - batch, - now, + uploadId = uploadId, + authenticatedUser = submissionParams.authenticatedUser, + groupName = submissionParams.groupName, + submittedOrganism = submissionParams.organism, + uploadedMetadataBatch = batch, + uploadedAt = now, ) } } + is SubmissionParams.RevisionSubmissionParams -> { revisionEntryStreamAsSequence(metadataStream) .chunked(batchSize) .forEach { batch -> uploadDatabaseService.batchInsertRevisedMetadataInAuxTable( - uploadId, - submissionParams.username, - submissionParams.organism, - batch, - now, + uploadId = uploadId, + authenticatedUser = submissionParams.authenticatedUser, + submittedOrganism = submissionParams.organism, + uploadedRevisedMetadataBatch = batch, + uploadedAt = now, ) } } @@ -286,11 +288,13 @@ class SubmitModel( } } + val allowedCompressionFormats = expectedFileType.getCompressedExtensions() + .filter { it.key != CompressionAlgorithm.NONE } + .flatMap { it.value }.joinToString(", .") throw BadRequestException( "${expectedFileType.displayName} has wrong extension. Must be " + ".${expectedFileType.validExtensions.joinToString(", .")} for uncompressed submissions or " + - ".${expectedFileType.getCompressedExtensions().filter { it.key != CompressionAlgorithm.NONE } - .flatMap { it.value }.joinToString(", .")} for compressed submissions", + ".$allowedCompressionFormats for compressed submissions", ) } diff --git a/backend/src/main/kotlin/org/loculus/backend/service/datauseterms/DataUseTermsDatabaseService.kt b/backend/src/main/kotlin/org/loculus/backend/service/datauseterms/DataUseTermsDatabaseService.kt index 1f1b49a3c7..b9756d1ca1 100644 --- a/backend/src/main/kotlin/org/loculus/backend/service/datauseterms/DataUseTermsDatabaseService.kt +++ b/backend/src/main/kotlin/org/loculus/backend/service/datauseterms/DataUseTermsDatabaseService.kt @@ -8,6 +8,7 @@ import org.jetbrains.exposed.sql.select import org.loculus.backend.api.DataUseTerms import org.loculus.backend.api.DataUseTermsHistoryEntry import org.loculus.backend.api.DataUseTermsType +import org.loculus.backend.auth.AuthenticatedUser import org.loculus.backend.controller.NotFoundException import org.loculus.backend.service.submission.AccessionPreconditionValidator import org.loculus.backend.utils.Accession @@ -21,11 +22,15 @@ class DataUseTermsDatabaseService( private val dataUseTermsPreconditionValidator: DataUseTermsPreconditionValidator, ) { - fun setNewDataUseTerms(username: String, accessions: List, newDataUseTerms: DataUseTerms) { + fun setNewDataUseTerms( + authenticatedUser: AuthenticatedUser, + accessions: List, + newDataUseTerms: DataUseTerms, + ) { val now = Clock.System.now().toLocalDateTime(TimeZone.UTC) accessionPreconditionValidator.validateAccessions( - submitter = username, + authenticatedUser = authenticatedUser, accessions = accessions, ) @@ -40,7 +45,7 @@ class DataUseTermsDatabaseService( is DataUseTerms.Restricted -> newDataUseTerms.restrictedUntil else -> null } - this[DataUseTermsTable.userNameColumn] = username + this[DataUseTermsTable.userNameColumn] = authenticatedUser.username } } diff --git a/backend/src/main/kotlin/org/loculus/backend/service/groupmanagement/GroupManagementDatabaseService.kt b/backend/src/main/kotlin/org/loculus/backend/service/groupmanagement/GroupManagementDatabaseService.kt index b19dc945c8..404d7f87ee 100644 --- a/backend/src/main/kotlin/org/loculus/backend/service/groupmanagement/GroupManagementDatabaseService.kt +++ b/backend/src/main/kotlin/org/loculus/backend/service/groupmanagement/GroupManagementDatabaseService.kt @@ -12,6 +12,7 @@ import org.loculus.backend.api.Address import org.loculus.backend.api.Group import org.loculus.backend.api.GroupDetails import org.loculus.backend.api.User +import org.loculus.backend.auth.AuthenticatedUser import org.loculus.backend.controller.ConflictException import org.loculus.backend.controller.NotFoundException import org.loculus.backend.model.UNIQUE_CONSTRAINT_VIOLATION_SQL_STATE @@ -51,7 +52,7 @@ class GroupManagementDatabaseService( ) } - fun createNewGroup(group: Group, username: String) { + fun createNewGroup(group: Group, authenticatedUser: AuthenticatedUser) { try { GroupsTable.insert { it[groupNameColumn] = group.groupName @@ -74,41 +75,47 @@ class GroupManagementDatabaseService( } UserGroupsTable.insert { - it[userNameColumn] = username + it[userNameColumn] = authenticatedUser.username it[groupNameColumn] = group.groupName } } - fun getGroupsOfUser(username: String): List { - return UserGroupsTable.join( - GroupsTable, - JoinType.LEFT, - additionalConstraint = { - (UserGroupsTable.groupNameColumn eq GroupsTable.groupNameColumn) - }, - ) - .select { UserGroupsTable.userNameColumn eq username } - .map { - Group( - groupName = it[GroupsTable.groupNameColumn], - institution = it[GroupsTable.institutionColumn], - address = Address( - line1 = it[GroupsTable.addressLine1], - line2 = it[GroupsTable.addressLine2], - postalCode = it[GroupsTable.addressPostalCode], - city = it[GroupsTable.addressCity], - state = it[GroupsTable.addressState], - country = it[GroupsTable.addressCountry], - ), - contactEmail = it[GroupsTable.contactEmailColumn], - ) - } + fun getGroupsOfUser(authenticatedUser: AuthenticatedUser): List { + val groupsQuery = when (authenticatedUser.isSuperUser) { + true -> GroupsTable.selectAll() + false -> + UserGroupsTable + .join( + GroupsTable, + JoinType.LEFT, + additionalConstraint = { + (UserGroupsTable.groupNameColumn eq GroupsTable.groupNameColumn) + }, + ) + .select { UserGroupsTable.userNameColumn eq authenticatedUser.username } + } + + return groupsQuery.map { + Group( + groupName = it[GroupsTable.groupNameColumn], + institution = it[GroupsTable.institutionColumn], + address = Address( + line1 = it[GroupsTable.addressLine1], + line2 = it[GroupsTable.addressLine2], + postalCode = it[GroupsTable.addressPostalCode], + city = it[GroupsTable.addressCity], + state = it[GroupsTable.addressState], + country = it[GroupsTable.addressCountry], + ), + contactEmail = it[GroupsTable.contactEmailColumn], + ) + } } - fun addUserToGroup(groupMember: String, groupName: String, usernameToAdd: String) { + fun addUserToGroup(authenticatedUser: AuthenticatedUser, groupName: String, usernameToAdd: String) { groupManagementPreconditionValidator.validateThatUserExists(usernameToAdd) - groupManagementPreconditionValidator.validateUserInExistingGroup(groupName, groupMember) + groupManagementPreconditionValidator.validateUserIsAllowedToModifyGroup(groupName, authenticatedUser) try { UserGroupsTable.insert { @@ -125,8 +132,8 @@ class GroupManagementDatabaseService( } } - fun removeUserFromGroup(groupMember: String, groupName: String, usernameToRemove: String) { - groupManagementPreconditionValidator.validateUserInExistingGroup(groupName, groupMember) + fun removeUserFromGroup(authenticatedUser: AuthenticatedUser, groupName: String, usernameToRemove: String) { + groupManagementPreconditionValidator.validateUserIsAllowedToModifyGroup(groupName, authenticatedUser) UserGroupsTable.deleteWhere { (userNameColumn eq usernameToRemove) and diff --git a/backend/src/main/kotlin/org/loculus/backend/service/groupmanagement/GroupManagementPreconditionValidator.kt b/backend/src/main/kotlin/org/loculus/backend/service/groupmanagement/GroupManagementPreconditionValidator.kt index 24e5b3024b..c50203a0c2 100644 --- a/backend/src/main/kotlin/org/loculus/backend/service/groupmanagement/GroupManagementPreconditionValidator.kt +++ b/backend/src/main/kotlin/org/loculus/backend/service/groupmanagement/GroupManagementPreconditionValidator.kt @@ -2,6 +2,7 @@ package org.loculus.backend.service.groupmanagement import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.select +import org.loculus.backend.auth.AuthenticatedUser import org.loculus.backend.controller.ForbiddenException import org.loculus.backend.controller.NotFoundException import org.loculus.backend.service.KeycloakAdapter @@ -13,13 +14,17 @@ class GroupManagementPreconditionValidator( private val keycloakAdapter: KeycloakAdapter, ) { - @Transactional - fun validateUserInExistingGroup(groupName: String, groupMember: String) { - validateUserInExistingGroups(listOf(groupName), groupMember) + @Transactional(readOnly = true) + fun validateUserIsAllowedToModifyGroup(groupName: String, authenticatedUser: AuthenticatedUser) { + validateUserIsAllowedToModifyGroups(listOf(groupName), authenticatedUser) } - @Transactional - fun validateUserInExistingGroups(groupNames: List, groupMember: String) { + @Transactional(readOnly = true) + fun validateUserIsAllowedToModifyGroups(groupNames: List, authenticatedUser: AuthenticatedUser) { + if (authenticatedUser.isSuperUser) { + return + } + val existingGroups = GroupsTable .select { GroupsTable.groupNameColumn inList groupNames } .map { it[GroupsTable.groupNameColumn] } @@ -31,10 +36,11 @@ class GroupManagementPreconditionValidator( throw NotFoundException("Group(s) ${nonExistingGroups.joinToString()} do not exist.") } + val username = authenticatedUser.username val userGroups = UserGroupsTable .select { (UserGroupsTable.groupNameColumn inList existingGroups) and - (UserGroupsTable.userNameColumn eq groupMember) + (UserGroupsTable.userNameColumn eq username) } .map { it[UserGroupsTable.groupNameColumn] } .toSet() @@ -43,7 +49,7 @@ class GroupManagementPreconditionValidator( if (missingGroups.isNotEmpty()) { throw ForbiddenException( - "User $groupMember is not a member of group(s) ${missingGroups.joinToString()}. Action not allowed.", + "User $username is not a member of group(s) ${missingGroups.joinToString()}. Action not allowed.", ) } } diff --git a/backend/src/main/kotlin/org/loculus/backend/service/seqsetcitations/SeqSetCitationsDatabaseService.kt b/backend/src/main/kotlin/org/loculus/backend/service/seqsetcitations/SeqSetCitationsDatabaseService.kt index cd5de2a1eb..b934173c83 100644 --- a/backend/src/main/kotlin/org/loculus/backend/service/seqsetcitations/SeqSetCitationsDatabaseService.kt +++ b/backend/src/main/kotlin/org/loculus/backend/service/seqsetcitations/SeqSetCitationsDatabaseService.kt @@ -29,6 +29,7 @@ import org.loculus.backend.api.SeqSetRecord import org.loculus.backend.api.SequenceEntryStatus import org.loculus.backend.api.Status.APPROVED_FOR_RELEASE import org.loculus.backend.api.SubmittedSeqSetRecord +import org.loculus.backend.auth.AuthenticatedUser import org.loculus.backend.controller.NotFoundException import org.loculus.backend.controller.UnprocessableEntityException import org.loculus.backend.service.submission.AccessionPreconditionValidator @@ -51,12 +52,12 @@ class SeqSetCitationsDatabaseService( } fun createSeqSet( - username: String, + authenticatedUser: AuthenticatedUser, seqSetName: String, seqSetRecords: List, seqSetDescription: String?, ): ResponseSeqSet { - log.info { "Create seqSet $seqSetName, user $username" } + log.info { "Create seqSet $seqSetName, user ${authenticatedUser.username}" } validateSeqSetName(seqSetName) validateSeqSetRecords(seqSetRecords) @@ -69,7 +70,7 @@ class SeqSetCitationsDatabaseService( it[SeqSetsTable.description] = seqSetDescription ?: "" it[SeqSetsTable.seqSetVersion] = 1 it[SeqSetsTable.createdAt] = now - it[SeqSetsTable.createdBy] = username + it[SeqSetsTable.createdBy] = authenticatedUser.username } for (record in seqSetRecords) { @@ -93,12 +94,13 @@ class SeqSetCitationsDatabaseService( } fun updateSeqSet( - username: String, + authenticatedUser: AuthenticatedUser, seqSetId: String, seqSetName: String, seqSetRecords: List, seqSetDescription: String?, ): ResponseSeqSet { + val username = authenticatedUser.username log.info { "Update seqSet $seqSetId, user $username" } validateSeqSetName(seqSetName) @@ -241,7 +243,8 @@ class SeqSetCitationsDatabaseService( return selectedSeqSetRecords } - fun getSeqSets(username: String): List { + fun getSeqSets(authenticatedUser: AuthenticatedUser): List { + val username = authenticatedUser.username log.info { "Get seqSets for user $username" } val selectedSeqSets = SeqSetsTable @@ -260,7 +263,8 @@ class SeqSetCitationsDatabaseService( } } - fun deleteSeqSet(username: String, seqSetId: String, version: Long) { + fun deleteSeqSet(authenticatedUser: AuthenticatedUser, seqSetId: String, version: Long) { + val username = authenticatedUser.username log.info { "Delete seqSet $seqSetId, version $version, user $username" } val seqSetUuid = UUID.fromString(seqSetId) @@ -315,7 +319,8 @@ class SeqSetCitationsDatabaseService( } } - fun createSeqSetDOI(username: String, seqSetId: String, version: Long): ResponseSeqSet { + fun createSeqSetDOI(authenticatedUser: AuthenticatedUser, seqSetId: String, version: Long): ResponseSeqSet { + val username = authenticatedUser.username log.info { "Create DOI for seqSet $seqSetId, version $version, user $username" } validateCreateSeqSetDOI(username, seqSetId, version) diff --git a/backend/src/main/kotlin/org/loculus/backend/service/submission/AccessionPreconditionValidator.kt b/backend/src/main/kotlin/org/loculus/backend/service/submission/AccessionPreconditionValidator.kt index 105c89423c..3620a708f2 100644 --- a/backend/src/main/kotlin/org/loculus/backend/service/submission/AccessionPreconditionValidator.kt +++ b/backend/src/main/kotlin/org/loculus/backend/service/submission/AccessionPreconditionValidator.kt @@ -7,6 +7,7 @@ import org.loculus.backend.api.AccessionVersion import org.loculus.backend.api.AccessionVersionInterface import org.loculus.backend.api.Organism import org.loculus.backend.api.Status +import org.loculus.backend.auth.AuthenticatedUser import org.loculus.backend.controller.ForbiddenException import org.loculus.backend.controller.UnprocessableEntityException import org.loculus.backend.service.groupmanagement.GroupManagementPreconditionValidator @@ -23,7 +24,7 @@ class AccessionPreconditionValidator( ) { fun validateAccessionVersions( - submitter: String, + authenticatedUser: AuthenticatedUser, accessionVersions: List, statuses: List, organism: Organism, @@ -42,7 +43,7 @@ class AccessionPreconditionValidator( validateAccessionVersionsExist(sequenceEntries, accessionVersions, table) validateSequenceEntriesAreInStates(sequenceEntries, statuses, table) - validateUserIsAllowedToEditSequenceEntries(sequenceEntries, submitter, table) + validateUserIsAllowedToEditSequenceEntries(sequenceEntries, authenticatedUser, table) validateOrganism(sequenceEntries, organism, table) } } @@ -63,7 +64,7 @@ class AccessionPreconditionValidator( } fun validateAccessions( - submitter: String, + authenticatedUser: AuthenticatedUser, accessions: List, statuses: List, organism: Organism, @@ -84,12 +85,12 @@ class AccessionPreconditionValidator( validateAccessionsExist(sequenceEntries, accessions, table) validateSequenceEntriesAreInStates(sequenceEntries, statuses, table) - validateUserIsAllowedToEditSequenceEntries(sequenceEntries, submitter, table) + validateUserIsAllowedToEditSequenceEntries(sequenceEntries, authenticatedUser, table) validateOrganism(sequenceEntries, organism, table) } } - fun validateAccessions(submitter: String, accessions: List) { + fun validateAccessions(authenticatedUser: AuthenticatedUser, accessions: List) { sequenceEntriesViewProvider.get(organism = null).let { table -> val sequenceEntries = table .slice( @@ -103,7 +104,7 @@ class AccessionPreconditionValidator( ) validateAccessionsExist(sequenceEntries, accessions, table) - validateUserIsAllowedToEditSequenceEntries(sequenceEntries, submitter, table) + validateUserIsAllowedToEditSequenceEntries(sequenceEntries, authenticatedUser, table) } } @@ -185,9 +186,13 @@ class AccessionPreconditionValidator( private fun validateUserIsAllowedToEditSequenceEntries( sequenceEntries: Query, - submitter: String, + authenticatedUser: AuthenticatedUser, table: SequenceEntriesView, ) { + if (authenticatedUser.isSuperUser) { + return + } + val groupsOfSequenceEntries = sequenceEntries .groupBy( { @@ -200,7 +205,7 @@ class AccessionPreconditionValidator( groupsOfSequenceEntries.forEach { (groupName, accessionList) -> try { - groupManagementPreconditionValidator.validateUserInExistingGroup(groupName, submitter) + groupManagementPreconditionValidator.validateUserIsAllowedToModifyGroup(groupName, authenticatedUser) } catch (error: ForbiddenException) { throw ForbiddenException( error.message + " Affected AccessionVersions: " + accessionList.map { diff --git a/backend/src/main/kotlin/org/loculus/backend/service/submission/SubmissionDatabaseService.kt b/backend/src/main/kotlin/org/loculus/backend/service/submission/SubmissionDatabaseService.kt index 3d18493bf2..16a440fd94 100644 --- a/backend/src/main/kotlin/org/loculus/backend/service/submission/SubmissionDatabaseService.kt +++ b/backend/src/main/kotlin/org/loculus/backend/service/submission/SubmissionDatabaseService.kt @@ -43,6 +43,7 @@ import org.loculus.backend.api.SubmissionIdMapping import org.loculus.backend.api.SubmittedProcessedData import org.loculus.backend.api.UnprocessedData import org.loculus.backend.api.WarningsFilter +import org.loculus.backend.auth.AuthenticatedUser import org.loculus.backend.controller.BadRequestException import org.loculus.backend.controller.ProcessingValidationException import org.loculus.backend.controller.UnprocessableEntityException @@ -228,22 +229,22 @@ class SubmissionDatabaseService( } fun approveProcessedData( - submitter: String, + authenticatedUser: AuthenticatedUser, accessionVersionsFilter: List?, organism: Organism, scope: ApproveDataScope, ): List { if (accessionVersionsFilter == null) { - log.info { "approving all sequences by all groups $submitter is member of" } + log.info { "approving all sequences by all groups ${authenticatedUser.username} is member of" } } else { - log.info { "approving ${accessionVersionsFilter.size} sequences by $submitter" } + log.info { "approving ${accessionVersionsFilter.size} sequences by ${authenticatedUser.username}" } } val now = Clock.System.now().toLocalDateTime(TimeZone.UTC) if (accessionVersionsFilter != null) { accessionPreconditionValidator.validateAccessionVersions( - submitter, + authenticatedUser, accessionVersionsFilter, listOf(Status.AWAITING_APPROVAL), organism, @@ -268,8 +269,10 @@ class SubmissionDatabaseService( val accessionCondition = if (accessionVersionsFilter !== null) { view.accessionVersionIsIn(accessionVersionsFilter) + } else if (authenticatedUser.isSuperUser) { + Op.TRUE } else { - view.groupIsOneOf(groupManagementDatabaseService.getGroupsOfUser(submitter)) + view.groupIsOneOf(groupManagementDatabaseService.getGroupsOfUser(authenticatedUser)) } val scopeCondition = if (scope == ApproveDataScope.WITHOUT_WARNINGS) { @@ -373,7 +376,7 @@ class SubmissionDatabaseService( } fun getSequences( - username: String, + authenticatedUser: AuthenticatedUser, organism: Organism?, groupsFilter: List?, statusesFilter: List?, @@ -382,20 +385,29 @@ class SubmissionDatabaseService( size: Int? = null, ): GetSequenceResponse { log.info { - "getting sequence for user $username (groupFilter: $groupsFilter in statuses $statusesFilter)." + + "getting sequence for user ${authenticatedUser.username} " + + "(groupFilter: $groupsFilter in statuses $statusesFilter)." + " Page $page of size $size " } - val validatedGroupNames = if (groupsFilter == null) { - groupManagementDatabaseService.getGroupsOfUser(username).map { it.groupName } - } else { - groupManagementPreconditionValidator.validateUserInExistingGroups(groupsFilter, username) - groupsFilter - } - val listOfStatuses = statusesFilter ?: Status.entries entriesViewProvider.get(organism).let { view -> + val groupCondition = if (groupsFilter != null) { + groupManagementPreconditionValidator.validateUserIsAllowedToModifyGroups( + groupsFilter, + authenticatedUser, + ) + view.groupNameIsOneOf(groupsFilter) + } else if (authenticatedUser.isSuperUser) { + Op.TRUE + } else { + val groupsOfUser = groupManagementDatabaseService + .getGroupsOfUser(authenticatedUser) + .map { it.groupName } + view.groupNameIsOneOf(groupsOfUser) + } + val baseQuery = view .join( DataUseTermsTable, @@ -412,6 +424,7 @@ class SubmissionDatabaseService( view.statusColumn, view.isRevocationColumn, view.groupNameColumn, + view.submitterColumn, view.organismColumn, view.submittedAtColumn, DataUseTermsTable.dataUseTermsTypeColumn, @@ -419,7 +432,7 @@ class SubmissionDatabaseService( ) .select( where = { - view.groupNameIsOneOf(validatedGroupNames) + groupCondition }, ) .orderBy(view.accessionColumn) @@ -452,12 +465,13 @@ class SubmissionDatabaseService( sequenceEntries = pagedQuery .map { row -> SequenceEntryStatus( - row[view.accessionColumn], - row[view.versionColumn], - Status.fromString(row[view.statusColumn]), - row[view.groupNameColumn], - row[view.isRevocationColumn], - row[view.submissionIdColumn], + accession = row[view.accessionColumn], + version = row[view.versionColumn], + status = Status.fromString(row[view.statusColumn]), + group = row[view.groupNameColumn], + submitter = row[view.submitterColumn], + isRevocation = row[view.isRevocationColumn], + submissionId = row[view.submissionIdColumn], dataUseTerms = DataUseTerms.fromParameters( DataUseTermsType.fromString(row[DataUseTermsTable.dataUseTermsTypeColumn]), row[DataUseTermsTable.restrictedUntilColumn], @@ -469,11 +483,15 @@ class SubmissionDatabaseService( } } - fun revoke(accessions: List, username: String, organism: Organism): List { + fun revoke( + accessions: List, + authenticatedUser: AuthenticatedUser, + organism: Organism, + ): List { log.info { "revoking ${accessions.size} sequences" } accessionPreconditionValidator.validateAccessions( - username, + authenticatedUser, accessions, listOf(Status.APPROVED_FOR_RELEASE), organism, @@ -539,14 +557,18 @@ class SubmissionDatabaseService( fun deleteSequenceEntryVersions( accessionVersionsFilter: List?, - submitter: String, + authenticatedUser: AuthenticatedUser, organism: Organism, scope: DeleteSequenceScope, ): List { if (accessionVersionsFilter == null) { - log.info { "deleting all sequences of all groups $submitter is member of in the scope $scope" } + log.info { + "deleting all sequences of all groups ${authenticatedUser.username} is member of in the scope $scope" + } } else { - log.info { "deleting ${accessionVersionsFilter.size} sequences by $submitter in scope $scope" } + log.info { + "deleting ${accessionVersionsFilter.size} sequences by ${authenticatedUser.username} in scope $scope" + } } val listOfDeletableStatuses = listOf( @@ -557,7 +579,7 @@ class SubmissionDatabaseService( if (accessionVersionsFilter != null) { accessionPreconditionValidator.validateAccessionVersions( - submitter, + authenticatedUser, accessionVersionsFilter, listOfDeletableStatuses, organism, @@ -576,8 +598,10 @@ class SubmissionDatabaseService( ).select { val accessionCondition = if (accessionVersionsFilter != null) { table.accessionVersionIsIn(accessionVersionsFilter) + } else if (authenticatedUser.isSuperUser) { + Op.TRUE } else { - table.groupIsOneOf(groupManagementDatabaseService.getGroupsOfUser(submitter)) + table.groupIsOneOf(groupManagementDatabaseService.getGroupsOfUser(authenticatedUser)) } val scopeCondition = when (scope) { @@ -607,11 +631,15 @@ class SubmissionDatabaseService( return sequenceEntriesToDelete } - fun submitEditedData(submitter: String, editedAccessionVersion: UnprocessedData, organism: Organism) { + fun submitEditedData( + authenticatedUser: AuthenticatedUser, + editedAccessionVersion: UnprocessedData, + organism: Organism, + ) { log.info { "edited sequence entry submitted $editedAccessionVersion" } accessionPreconditionValidator.validateAccessionVersions( - submitter, + authenticatedUser, listOf(editedAccessionVersion), listOf(Status.AWAITING_APPROVAL, Status.HAS_ERRORS), organism, @@ -625,16 +653,17 @@ class SubmissionDatabaseService( } fun getSequenceEntryVersionToEdit( - submitter: String, + authenticatedUser: AuthenticatedUser, accessionVersion: AccessionVersion, organism: Organism, ): SequenceEntryVersionToEdit { log.info { - "Getting sequence entry ${accessionVersion.displayAccessionVersion()} by $submitter to edit" + "Getting sequence entry ${accessionVersion.displayAccessionVersion()} " + + "by ${authenticatedUser.username} to edit" } accessionPreconditionValidator.validateAccessionVersions( - submitter, + authenticatedUser, listOf(accessionVersion), listOf(Status.HAS_ERRORS, Status.AWAITING_APPROVAL), organism, diff --git a/backend/src/main/kotlin/org/loculus/backend/service/submission/UploadDatabaseService.kt b/backend/src/main/kotlin/org/loculus/backend/service/submission/UploadDatabaseService.kt index 72259e4982..8ef0f00f24 100644 --- a/backend/src/main/kotlin/org/loculus/backend/service/submission/UploadDatabaseService.kt +++ b/backend/src/main/kotlin/org/loculus/backend/service/submission/UploadDatabaseService.kt @@ -15,6 +15,7 @@ import org.jetbrains.exposed.sql.update import org.loculus.backend.api.Organism import org.loculus.backend.api.Status import org.loculus.backend.api.SubmissionIdMapping +import org.loculus.backend.auth.AuthenticatedUser import org.loculus.backend.model.SubmissionId import org.loculus.backend.model.SubmissionParams import org.loculus.backend.service.GenerateAccessionFromNumberService @@ -53,14 +54,14 @@ class UploadDatabaseService( fun batchInsertMetadataInAuxTable( uploadId: String, - username: String, + authenticatedUser: AuthenticatedUser, groupName: String, submittedOrganism: Organism, uploadedMetadataBatch: List, uploadedAt: LocalDateTime, ) { MetadataUploadAuxTable.batchInsert(uploadedMetadataBatch) { - this[submitterColumn] = username + this[submitterColumn] = authenticatedUser.username this[groupNameColumn] = groupName this[uploadedAtColumn] = uploadedAt this[submissionIdColumn] = it.submissionId @@ -72,14 +73,14 @@ class UploadDatabaseService( fun batchInsertRevisedMetadataInAuxTable( uploadId: String, - submitter: String, + authenticatedUser: AuthenticatedUser, submittedOrganism: Organism, uploadedRevisedMetadataBatch: List, uploadedAt: LocalDateTime, ) { MetadataUploadAuxTable.batchInsert(uploadedRevisedMetadataBatch) { this[accessionColumn] = it.accession - this[submitterColumn] = submitter + this[submitterColumn] = authenticatedUser.username this[uploadedAtColumn] = uploadedAt this[submissionIdColumn] = it.submissionId this[metadataColumn] = it.metadata @@ -180,7 +181,7 @@ class UploadDatabaseService( if (submissionParams is SubmissionParams.OriginalSubmissionParams) { dataUseTermsDatabaseService.setNewDataUseTerms( - submissionParams.username, + submissionParams.authenticatedUser, insertionResult.map { it.accession }, submissionParams.dataUseTerms, ) @@ -196,7 +197,11 @@ class UploadDatabaseService( SequenceUploadAuxTable.deleteWhere { sequenceUploadIdColumn eq uploadId } } - fun associateRevisedDataWithExistingSequenceEntries(uploadId: String, organism: Organism, username: String) { + fun associateRevisedDataWithExistingSequenceEntries( + uploadId: String, + organism: Organism, + authenticatedUser: AuthenticatedUser, + ) { val accessions = MetadataUploadAuxTable .slice(accessionColumn) @@ -204,7 +209,7 @@ class UploadDatabaseService( .map { it[accessionColumn]!! } accessionPreconditionValidator.validateAccessions( - username, + authenticatedUser, accessions, listOf(Status.APPROVED_FOR_RELEASE), organism, @@ -231,7 +236,7 @@ class UploadDatabaseService( } } - fun generateNewAccessionsForOriginalUpload(uploadId: String, organism: Organism, username: String) { + fun generateNewAccessionsForOriginalUpload(uploadId: String, organism: Organism) { val submissionIds = MetadataUploadAuxTable .slice(submissionIdColumn) @@ -251,12 +256,7 @@ class UploadDatabaseService( val submissionIdToAccessionMap = submissionIds.zip(nextAccessions) log.info { - "Generated ${submissionIdToAccessionMap.size} new accessions for original upload with UploadId " + - "$uploadId: ${submissionIdToAccessionMap.joinToString( - limit = 10, - ){ - it.toString() - } }" + "Generated ${submissionIdToAccessionMap.size} new accessions for original upload with UploadId $uploadId:" } submissionIdToAccessionMap.forEach { (submissionId, accession) -> diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/EndpointTestExtension.kt b/backend/src/test/kotlin/org/loculus/backend/controller/EndpointTestExtension.kt index 2329bb8e97..c8f5e775ff 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/EndpointTestExtension.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/EndpointTestExtension.kt @@ -13,7 +13,6 @@ import org.loculus.backend.api.Group import org.loculus.backend.controller.datauseterms.DataUseTermsControllerClient import org.loculus.backend.controller.groupmanagement.GroupManagementControllerClient import org.loculus.backend.controller.seqsetcitations.SeqSetCitationsControllerClient -import org.loculus.backend.controller.submission.DEFAULT_USER_NAME import org.loculus.backend.controller.submission.SubmissionControllerClient import org.loculus.backend.controller.submission.SubmissionConvenienceClient import org.loculus.backend.service.datauseterms.DATA_USE_TERMS_TABLE_NAME @@ -69,8 +68,12 @@ val DEFAULT_GROUP = Group( ), contactEmail = "testEmail", ) + +const val DEFAULT_USER_NAME = "testuser" +const val SUPER_USER_NAME = "test_superuser" const val ALTERNATIVE_DEFAULT_GROUP_NAME = "testGroup2" const val ALTERNATIVE_DEFAULT_USER_NAME = "testUser2" + val ALTERNATIVE_DEFAULT_GROUP = Group( groupName = ALTERNATIVE_DEFAULT_GROUP_NAME, institution = "alternativeTestInstitution", diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/RequestAuthorization.kt b/backend/src/test/kotlin/org/loculus/backend/controller/RequestAuthorization.kt index ce35b62d04..f843dd9af6 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/RequestAuthorization.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/RequestAuthorization.kt @@ -1,7 +1,9 @@ package org.loculus.backend.controller import io.jsonwebtoken.Jwts -import org.loculus.backend.controller.submission.DEFAULT_USER_NAME +import org.loculus.backend.auth.Roles.GET_RELEASED_DATA +import org.loculus.backend.auth.Roles.PREPROCESSING_PIPELINE +import org.loculus.backend.auth.Roles.SUPER_USER import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder import java.security.KeyPair import java.time.Instant @@ -11,8 +13,9 @@ import java.util.Date val keyPair: KeyPair = Jwts.SIG.RS256.keyPair().build() val jwtForDefaultUser = generateJwtFor(DEFAULT_USER_NAME) -val jwtForProcessingPipeline = generateJwtFor("preprocessing_pipeline", listOf("preprocessing_pipeline")) -val jwtForGetReleasedData = generateJwtFor("silo_import_job", listOf("get_released_data")) +val jwtForProcessingPipeline = generateJwtFor("preprocessing_pipeline", listOf(PREPROCESSING_PIPELINE)) +val jwtForGetReleasedData = generateJwtFor("silo_import_job", listOf(GET_RELEASED_DATA)) +val jwtForSuperUser = generateJwtFor(SUPER_USER_NAME, listOf(SUPER_USER)) fun generateJwtFor(username: String, roles: List = emptyList()): String = Jwts.builder() .expiration(Date.from(Instant.now().plus(1, ChronoUnit.DAYS))) diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/datauseterms/DataUseTermsControllerTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/datauseterms/DataUseTermsControllerTest.kt index 003ed2341e..7208ae41d7 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/datauseterms/DataUseTermsControllerTest.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/datauseterms/DataUseTermsControllerTest.kt @@ -12,8 +12,12 @@ import org.junit.jupiter.params.provider.MethodSource import org.loculus.backend.api.DataUseTerms import org.loculus.backend.api.DataUseTermsChangeRequest import org.loculus.backend.api.DataUseTermsType +import org.loculus.backend.controller.DEFAULT_GROUP_NAME +import org.loculus.backend.controller.DEFAULT_USER_NAME import org.loculus.backend.controller.EndpointTest import org.loculus.backend.controller.expectUnauthorizedResponse +import org.loculus.backend.controller.generateJwtFor +import org.loculus.backend.controller.jwtForSuperUser import org.loculus.backend.controller.submission.SubmissionConvenienceClient import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.MediaType @@ -46,6 +50,7 @@ class DataUseTermsControllerTest( @Test fun `GIVEN open submission WHEN getting data use terms THEN return history with one OPEN entry`() { val firstAccession = submissionConvenienceClient.submitDefaultFiles().first().accession + client.getDataUseTerms(firstAccession) .andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -123,6 +128,44 @@ class DataUseTermsControllerTest( } } + @Test + fun `WHEN I want to change data use terms of an entry of another group THEN is forbidden`() { + val accessions = submissionConvenienceClient + .submitDefaultFiles(username = DEFAULT_USER_NAME, groupName = DEFAULT_GROUP_NAME) + .map { it.accession } + + client.changeDataUseTerms( + DataUseTermsChangeRequest( + accessions = accessions, + newDataUseTerms = DataUseTerms.Open, + ), + jwt = generateJwtFor("user that is not a member of the group"), + ) + .andExpect(status().isForbidden) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("\$.detail", containsString("not a member of group(s) testGroup"))) + } + + @Test + fun `WHEN superuser changes data use terms of an entry of other group THEN is successful`() { + val accessions = submissionConvenienceClient + .submitDefaultFiles(username = DEFAULT_USER_NAME, groupName = DEFAULT_GROUP_NAME) + .map { it.accession } + + client.changeDataUseTerms( + DataUseTermsChangeRequest( + accessions = accessions, + newDataUseTerms = DataUseTerms.Open, + ), + jwt = jwtForSuperUser, + ) + .andExpect(status().isNoContent) + + client.getDataUseTerms(accession = accessions.first()) + .andExpect(jsonPath("\$[0].accession").value(accessions.first())) + .andExpect(jsonPath("\$[0].dataUseTerms.type").value(DataUseTermsType.OPEN.name)) + } + companion object { data class AuthScenario( val testFunction: (String?, DataUseTermsControllerClient) -> ResultActions, diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerClient.kt b/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerClient.kt index 4387bae7e3..c275d3c489 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerClient.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerClient.kt @@ -1,6 +1,7 @@ package org.loculus.backend.controller.groupmanagement import com.fasterxml.jackson.databind.ObjectMapper +import org.loculus.backend.api.Address import org.loculus.backend.api.Group import org.loculus.backend.controller.jwtForDefaultUser import org.loculus.backend.controller.withAuth @@ -12,6 +13,21 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +const val NEW_GROUP_NAME = "newGroup" +val NEW_GROUP = Group( + groupName = NEW_GROUP_NAME, + institution = "newInstitution", + address = Address( + line1 = "newAddressLine1", + line2 = "newAddressLine2", + city = "newCity", + state = "newState", + postalCode = "newPostalCode", + country = "newCountry", + ), + contactEmail = "newEmail", +) + class GroupManagementControllerClient(private val mockMvc: MockMvc, private val objectMapper: ObjectMapper) { fun createNewGroup(group: Group = NEW_GROUP, jwt: String? = jwtForDefaultUser): ResultActions = mockMvc.perform( post("/groups") @@ -33,8 +49,11 @@ class GroupManagementControllerClient(private val mockMvc: MockMvc, private val ) fun getGroupsOfUser(jwt: String? = jwtForDefaultUser): ResultActions = mockMvc.perform( - get("/groups") - .withAuth(jwt), + get("/user/groups").withAuth(jwt), + ) + + fun getAllGroups(jwt: String? = jwtForDefaultUser): ResultActions = mockMvc.perform( + get("/groups").withAuth(jwt), ) fun addUserToGroup( diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerTest.kt index 3887bb0d6d..a0589ed85e 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerTest.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerTest.kt @@ -5,22 +5,23 @@ import io.mockk.every import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.hasItem import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.Matchers.containsInAnyOrder import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource import org.keycloak.representations.idm.UserRepresentation -import org.loculus.backend.api.Address -import org.loculus.backend.api.Group import org.loculus.backend.controller.ALTERNATIVE_DEFAULT_GROUP import org.loculus.backend.controller.ALTERNATIVE_DEFAULT_GROUP_NAME import org.loculus.backend.controller.ALTERNATIVE_DEFAULT_USER_NAME import org.loculus.backend.controller.DEFAULT_GROUP import org.loculus.backend.controller.DEFAULT_GROUP_NAME +import org.loculus.backend.controller.DEFAULT_USER_NAME import org.loculus.backend.controller.EndpointTest import org.loculus.backend.controller.expectUnauthorizedResponse import org.loculus.backend.controller.generateJwtFor -import org.loculus.backend.controller.submission.DEFAULT_USER_NAME +import org.loculus.backend.controller.jwtForDefaultUser +import org.loculus.backend.controller.jwtForSuperUser import org.loculus.backend.service.KeycloakAdapter import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.MediaType @@ -29,21 +30,6 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status -const val NEW_GROUP_NAME = "newGroup" -val NEW_GROUP = Group( - groupName = NEW_GROUP_NAME, - institution = "newInstitution", - address = Address( - line1 = "newAddressLine1", - line2 = "newAddressLine2", - city = "newCity", - state = "newState", - postalCode = "newPostalCode", - country = "newCountry", - ), - contactEmail = "newEmail", -) - @EndpointTest class GroupManagementControllerTest( @Autowired private val client: GroupManagementControllerClient, @@ -82,6 +68,41 @@ class GroupManagementControllerTest( .andExpect(jsonPath("\$.users[*].name", hasItem(DEFAULT_USER_NAME))) } + @Test + fun `GIVEN I created a group WHEN I query my groups THEN returns created group`() { + val jwtForAnotherUser = generateJwtFor("another user") + + client.createNewGroup(group = NEW_GROUP, jwt = jwtForAnotherUser) + .andExpect(status().isNoContent) + + client.getGroupsOfUser(jwt = jwtForAnotherUser) + .andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("\$.size()", `is`(1))) + .andExpect(jsonPath("\$.[0].groupName").value(NEW_GROUP.groupName)) + .andExpect(jsonPath("\$.[0].institution").value(NEW_GROUP.institution)) + .andExpect(jsonPath("\$.[0].address.line1").value(NEW_GROUP.address.line1)) + .andExpect(jsonPath("\$.[0].address.line2").value(NEW_GROUP.address.line2)) + .andExpect(jsonPath("\$.[0].address.city").value(NEW_GROUP.address.city)) + .andExpect(jsonPath("\$.[0].address.state").value(NEW_GROUP.address.state)) + .andExpect(jsonPath("\$.[0].address.postalCode").value(NEW_GROUP.address.postalCode)) + .andExpect(jsonPath("\$.[0].address.country").value(NEW_GROUP.address.country)) + .andExpect(jsonPath("\$.[0].contactEmail").value(NEW_GROUP.contactEmail)) + } + + @Test + fun `WHEN superuser queries groups of user THEN returns all groups`() { + client.getGroupsOfUser(jwt = jwtForSuperUser) + .andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect( + jsonPath( + "\$.[*].groupName", + containsInAnyOrder(DEFAULT_GROUP_NAME, ALTERNATIVE_DEFAULT_GROUP_NAME), + ), + ) + } + @ParameterizedTest @MethodSource("authorizationTestCases") fun `GIVEN invalid authorization token WHEN performing action THEN returns 401 Unauthorized`(scenario: Scenario) { @@ -117,11 +138,11 @@ class GroupManagementControllerTest( } @Test - fun `GIVEN a group is created WHEN groups of the user are queried THEN expect that the group is returned`() { + fun `GIVEN a group is created WHEN all groups are queried THEN expect that the group is returned`() { client.createNewGroup() .andExpect(status().isNoContent) - client.getGroupsOfUser() + client.getAllGroups() .andExpect(status().isOk()) .andExpect { jsonPath("\$.size()", `is`(1)) } .andExpect { jsonPath("\$[0].groupName", `is`(NEW_GROUP.groupName)) } @@ -175,11 +196,26 @@ class GroupManagementControllerTest( } @Test - fun `GIVEN a non-member tries to remove another user THEN the action is forbidden`() { + fun `WHEN a non-member tries to remove another user THEN the action is forbidden`() { client.createNewGroup(jwt = generateJwtFor(ALTERNATIVE_DEFAULT_USER_NAME)) .andExpect(status().isNoContent) - client.addUserToGroup(DEFAULT_USER_NAME) + client.removeUserFromGroup(ALTERNATIVE_DEFAULT_USER_NAME, jwt = jwtForDefaultUser) + .andExpect(status().isForbidden) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect( + jsonPath("\$.detail").value( + "User $DEFAULT_USER_NAME is not a member of group(s) ${NEW_GROUP.groupName}. Action not allowed.", + ), + ) + } + + @Test + fun `WHEN a non-member tries to add another user THEN the action is forbidden`() { + client.createNewGroup(jwt = generateJwtFor(ALTERNATIVE_DEFAULT_USER_NAME)) + .andExpect(status().isNoContent) + + client.addUserToGroup(DEFAULT_USER_NAME, jwt = jwtForDefaultUser) .andExpect(status().isForbidden) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect( @@ -190,7 +226,31 @@ class GroupManagementControllerTest( } @Test - fun `GIVEN a non-exisiting group WHEN a user is added THEN expect to find no group`() { + fun `WHEN a superusers removes a user from a group THEN user is removed`() { + client.createNewGroup().andExpect(status().isNoContent) + + client.removeUserFromGroup(DEFAULT_USER_NAME, jwt = jwtForSuperUser) + .andExpect(status().isNoContent) + + client.getDetailsOfGroup() + .andExpect(status().isOk) + .andExpect(jsonPath("\$.users.size()", `is`(0))) + } + + @Test + fun `WHEN a superusers adds a user to a group THEN user is added`() { + client.createNewGroup().andExpect(status().isNoContent) + + client.addUserToGroup("another user", jwt = jwtForSuperUser) + .andExpect(status().isNoContent) + + client.getDetailsOfGroup() + .andExpect(status().isOk) + .andExpect(jsonPath("\$.users.size()", `is`(2))) + } + + @Test + fun `GIVEN a non-existing group WHEN a user is added THEN expect to find no group`() { client.addUserToGroup(DEFAULT_USER_NAME) .andExpect(status().isNotFound) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -273,11 +333,12 @@ class GroupManagementControllerTest( @JvmStatic fun authorizationTestCases(): List = listOf( - Scenario({ jwt, client -> client.createNewGroup(jwt = jwt) }, true), - Scenario({ jwt, client -> client.getDetailsOfGroup(jwt = jwt) }, false), - Scenario({ jwt, client -> client.getGroupsOfUser(jwt = jwt) }, false), - Scenario({ jwt, client -> client.addUserToGroup(DEFAULT_USER_NAME, jwt = jwt) }, true), - Scenario({ jwt, client -> client.removeUserFromGroup(DEFAULT_USER_NAME, jwt = jwt) }, true), + Scenario({ jwt, client -> client.createNewGroup(jwt = jwt) }, isModifying = true), + Scenario({ jwt, client -> client.getDetailsOfGroup(jwt = jwt) }, isModifying = false), + Scenario({ jwt, client -> client.getGroupsOfUser(jwt = jwt) }, isModifying = false), + Scenario({ jwt, client -> client.getAllGroups(jwt = jwt) }, isModifying = false), + Scenario({ jwt, client -> client.addUserToGroup(DEFAULT_USER_NAME, jwt = jwt) }, isModifying = true), + Scenario({ jwt, client -> client.removeUserFromGroup(DEFAULT_USER_NAME, jwt = jwt) }, isModifying = true), ) } } diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/seqsetcitations/CitationEndpointsTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/seqsetcitations/CitationEndpointsTest.kt index 9929079786..4d4c842f3d 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/seqsetcitations/CitationEndpointsTest.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/seqsetcitations/CitationEndpointsTest.kt @@ -92,13 +92,14 @@ class CitationEndpointsTest( } returns GetSequenceResponse( sequenceEntries = listOf( SequenceEntryStatus( - "mock-sequence-accession", - 1L, - Status.APPROVED_FOR_RELEASE, - "mock-group", - false, - "mock-submission-id", - DataUseTerms.Open, + accession = "mock-sequence-accession", + version = 1L, + status = Status.APPROVED_FOR_RELEASE, + group = "mock-group", + submitter = "mock-submitter", + isRevocation = false, + submissionId = "mock-submission-id", + dataUseTerms = DataUseTerms.Open, ), ), statusCounts = mapOf(Status.APPROVED_FOR_RELEASE to 1), diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/submission/ApproveProcessedDataEndpointTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/submission/ApproveProcessedDataEndpointTest.kt index 289017f1c4..0a677c81eb 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/submission/ApproveProcessedDataEndpointTest.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/submission/ApproveProcessedDataEndpointTest.kt @@ -6,17 +6,22 @@ import org.hamcrest.Matchers.hasSize import org.hamcrest.Matchers.`is` import org.junit.jupiter.api.Test import org.loculus.backend.api.AccessionVersion -import org.loculus.backend.api.ApproveDataScope +import org.loculus.backend.api.ApproveDataScope.ALL +import org.loculus.backend.api.ApproveDataScope.WITHOUT_WARNINGS import org.loculus.backend.api.Status.APPROVED_FOR_RELEASE import org.loculus.backend.api.Status.AWAITING_APPROVAL import org.loculus.backend.api.Status.IN_PROCESSING +import org.loculus.backend.controller.ALTERNATIVE_DEFAULT_GROUP_NAME +import org.loculus.backend.controller.DEFAULT_GROUP_NAME import org.loculus.backend.controller.DEFAULT_ORGANISM +import org.loculus.backend.controller.DEFAULT_USER_NAME import org.loculus.backend.controller.EndpointTest import org.loculus.backend.controller.OTHER_ORGANISM import org.loculus.backend.controller.assertStatusIs import org.loculus.backend.controller.expectUnauthorizedResponse import org.loculus.backend.controller.generateJwtFor import org.loculus.backend.controller.getAccessionVersions +import org.loculus.backend.controller.jwtForSuperUser import org.loculus.backend.controller.submission.SubmitFiles.DefaultFiles.NUMBER_OF_SEQUENCES import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.MediaType @@ -34,6 +39,7 @@ class ApproveProcessedDataEndpointTest( fun `GIVEN invalid authorization token THEN returns 401 Unauthorized`() { expectUnauthorizedResponse(isModifyingRequest = true) { client.approveProcessedSequenceEntries( + scope = ALL, emptyList(), jwt = it, ) @@ -44,7 +50,7 @@ class ApproveProcessedDataEndpointTest( fun `GIVEN sequence entries are processed WHEN I approve them THEN their status should be APPROVED_FOR_RELEASE`() { val accessionVersions = convenienceClient.prepareDataTo(AWAITING_APPROVAL).getAccessionVersions() - client.approveProcessedSequenceEntries(accessionVersions) + client.approveProcessedSequenceEntries(scope = ALL, accessionVersions) .andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("\$[*].accession").value(accessionVersions.map { it.accession })) @@ -59,7 +65,7 @@ class ApproveProcessedDataEndpointTest( fun `GIVEN revoked sequence entries awaiting approval THEN their status should be APPROVED_FOR_RELEASE`() { val accessionVersions = convenienceClient.prepareDefaultSequenceEntriesToAwaitingApprovalForRevocation() - client.approveProcessedSequenceEntries(accessionVersions) + client.approveProcessedSequenceEntries(scope = ALL, accessionVersions) .andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("\$[*].accession").value(accessionVersions.map { it.accession })) @@ -74,7 +80,7 @@ class ApproveProcessedDataEndpointTest( fun `WHEN I approve without accession filter or with full scope THEN all data is approved`() { val approvableSequences = convenienceClient.prepareDataTo(AWAITING_APPROVAL).map { it.accession } - client.approveProcessedSequenceEntries(scope = ApproveDataScope.ALL) + client.approveProcessedSequenceEntries(scope = ALL) .andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("\$[*].accession").value(approvableSequences)) @@ -89,7 +95,7 @@ class ApproveProcessedDataEndpointTest( fun `WHEN I approve sequence entries as non-group member THEN it should fail as forbidden`() { val accessionVersions = convenienceClient.prepareDataTo(AWAITING_APPROVAL).getAccessionVersions() - client.approveProcessedSequenceEntries(accessionVersions, jwt = generateJwtFor("other user")) + client.approveProcessedSequenceEntries(scope = ALL, accessionVersions, jwt = generateJwtFor("other user")) .andExpect(status().isForbidden) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect( @@ -113,6 +119,7 @@ class ApproveProcessedDataEndpointTest( val accessionVersions = convenienceClient.prepareDataTo(AWAITING_APPROVAL).getAccessionVersions() client.approveProcessedSequenceEntries( + scope = ALL, listOf( accessionVersions.first(), AccessionVersion(nonExistentAccession, 1), @@ -122,7 +129,7 @@ class ApproveProcessedDataEndpointTest( .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.detail", containsString("Accession versions 999.1 do not exist"))) - convenienceClient.getSequenceEntryOfUser(accessionVersions.first()).assertStatusIs(AWAITING_APPROVAL) + convenienceClient.getSequenceEntry(accessionVersions.first()).assertStatusIs(AWAITING_APPROVAL) } @Test @@ -131,6 +138,7 @@ class ApproveProcessedDataEndpointTest( val nonExistingVersion = accessionVersions[1].copy(version = 999L) client.approveProcessedSequenceEntries( + scope = ALL, listOf( accessionVersions.first(), nonExistingVersion, @@ -148,7 +156,7 @@ class ApproveProcessedDataEndpointTest( ), ) - convenienceClient.getSequenceEntryOfUser(accessionVersions.first()).assertStatusIs(AWAITING_APPROVAL) + convenienceClient.getSequenceEntry(accessionVersions.first()).assertStatusIs(AWAITING_APPROVAL) } @Test @@ -156,13 +164,14 @@ class ApproveProcessedDataEndpointTest( val accessionVersionsInCorrectState = convenienceClient.prepareDataTo(AWAITING_APPROVAL).getAccessionVersions() val accessionVersionNotInCorrectState = convenienceClient.prepareDataTo(IN_PROCESSING).getAccessionVersions() - convenienceClient.getSequenceEntryOfUser(accessionVersionsInCorrectState.first()) + convenienceClient.getSequenceEntry(accessionVersionsInCorrectState.first()) .assertStatusIs(AWAITING_APPROVAL) - convenienceClient.getSequenceEntryOfUser(accessionVersionNotInCorrectState.first()).assertStatusIs( + convenienceClient.getSequenceEntry(accessionVersionNotInCorrectState.first()).assertStatusIs( IN_PROCESSING, ) client.approveProcessedSequenceEntries( + scope = ALL, accessionVersionsInCorrectState + accessionVersionNotInCorrectState, ) .andExpect(status().isUnprocessableEntity) @@ -178,9 +187,9 @@ class ApproveProcessedDataEndpointTest( ), ) - convenienceClient.getSequenceEntryOfUser(accessionVersionsInCorrectState.first()) + convenienceClient.getSequenceEntry(accessionVersionsInCorrectState.first()) .assertStatusIs(AWAITING_APPROVAL) - convenienceClient.getSequenceEntryOfUser(accessionVersionNotInCorrectState.first()).assertStatusIs( + convenienceClient.getSequenceEntry(accessionVersionNotInCorrectState.first()).assertStatusIs( IN_PROCESSING, ) } @@ -191,6 +200,7 @@ class ApproveProcessedDataEndpointTest( val otherOrganismData = convenienceClient.prepareDataTo(AWAITING_APPROVAL, organism = OTHER_ORGANISM) client.approveProcessedSequenceEntries( + scope = ALL, defaultOrganismData.getAccessionVersions() + otherOrganismData.getAccessionVersions(), organism = OTHER_ORGANISM, ) @@ -201,13 +211,13 @@ class ApproveProcessedDataEndpointTest( .value(containsString("accession versions are not of organism otherOrganism")), ) - convenienceClient.getSequenceEntryOfUser( + convenienceClient.getSequenceEntry( accession = defaultOrganismData.first().accession, version = 1, organism = DEFAULT_ORGANISM, ) .assertStatusIs(AWAITING_APPROVAL) - convenienceClient.getSequenceEntryOfUser( + convenienceClient.getSequenceEntry( accession = otherOrganismData.first().accession, version = 1, organism = OTHER_ORGANISM, @@ -229,18 +239,18 @@ class ApproveProcessedDataEndpointTest( PreparedProcessedData.successfullyProcessed(accession = accessionOfSuccessfullyProcessedData), ) - client.approveProcessedSequenceEntries(scope = ApproveDataScope.WITHOUT_WARNINGS) + client.approveProcessedSequenceEntries(scope = WITHOUT_WARNINGS) .andExpect(status().isOk) - convenienceClient.getSequenceEntryOfUser(accession = accessionOfDataWithWarnings, version = 1) + convenienceClient.getSequenceEntry(accession = accessionOfDataWithWarnings, version = 1) .assertStatusIs(AWAITING_APPROVAL) - convenienceClient.getSequenceEntryOfUser(accession = accessionOfSuccessfullyProcessedData, version = 1) + convenienceClient.getSequenceEntry(accession = accessionOfSuccessfullyProcessedData, version = 1) .assertStatusIs(APPROVED_FOR_RELEASE) - client.approveProcessedSequenceEntries(scope = ApproveDataScope.ALL) + client.approveProcessedSequenceEntries(scope = ALL) .andExpect(status().isOk) - convenienceClient.getSequenceEntryOfUser(accession = accessionOfDataWithWarnings, version = 1) + convenienceClient.getSequenceEntry(accession = accessionOfDataWithWarnings, version = 1) .assertStatusIs(APPROVED_FOR_RELEASE) } @@ -264,19 +274,61 @@ class ApproveProcessedDataEndpointTest( ) client.approveProcessedSequenceEntries( - scope = ApproveDataScope.WITHOUT_WARNINGS, - listOfSequencesToApprove = listOf( + scope = WITHOUT_WARNINGS, + accessionVersionsFilter = listOf( AccessionVersion(accessionOfDataWithWarnings, 1), AccessionVersion(accessionOfSuccessfullyProcessedData, 1), ), ) .andExpect(status().isOk) - convenienceClient.getSequenceEntryOfUser(accession = accessionOfDataWithWarnings, version = 1) + convenienceClient.getSequenceEntry(accession = accessionOfDataWithWarnings, version = 1) .assertStatusIs(AWAITING_APPROVAL) - convenienceClient.getSequenceEntryOfUser(accession = accessionOfSuccessfullyProcessedData, version = 1) + convenienceClient.getSequenceEntry(accession = accessionOfSuccessfullyProcessedData, version = 1) .assertStatusIs(APPROVED_FOR_RELEASE) - convenienceClient.getSequenceEntryOfUser(accession = accessionOfAnotherSuccessfullyProcessedData, version = 1) + convenienceClient.getSequenceEntry(accession = accessionOfAnotherSuccessfullyProcessedData, version = 1) .assertStatusIs(AWAITING_APPROVAL) } + + @Test + fun `WHEN superuser approves all entries THEN is successfully approved`() { + val accessionVersions = convenienceClient + .prepareDataTo( + AWAITING_APPROVAL, + username = DEFAULT_USER_NAME, + groupName = DEFAULT_GROUP_NAME, + ) + + convenienceClient.prepareDataTo( + AWAITING_APPROVAL, + username = DEFAULT_USER_NAME, + groupName = ALTERNATIVE_DEFAULT_GROUP_NAME, + ) + + client.approveProcessedSequenceEntries(scope = ALL, jwt = jwtForSuperUser) + .andExpect(status().isOk) + .andExpect(jsonPath("\$.length()").value(accessionVersions.size)) + .andExpect(jsonPath("\$[*].accession").value(accessionVersions.map { it.accession })) + + convenienceClient.getSequenceEntry(accessionVersions.first()).assertStatusIs(APPROVED_FOR_RELEASE) + } + + @Test + fun `WHEN superuser approves entries of other user THEN is successfully approved`() { + val accessionVersions = convenienceClient.prepareDataTo( + AWAITING_APPROVAL, + username = DEFAULT_USER_NAME, + groupName = DEFAULT_GROUP_NAME, + ) + + client.approveProcessedSequenceEntries( + scope = ALL, + accessionVersionsFilter = accessionVersions, + jwt = jwtForSuperUser, + ) + .andExpect(status().isOk) + .andExpect(jsonPath("\$.length()").value(accessionVersions.size)) + .andExpect(jsonPath("\$[*].accession").value(accessionVersions.map { it.accession })) + + convenienceClient.getSequenceEntry(accessionVersions.first()).assertStatusIs(APPROVED_FOR_RELEASE) + } } diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/submission/DeleteSequencesEndpointTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/submission/DeleteSequencesEndpointTest.kt index fdae1877f7..1c1e153917 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/submission/DeleteSequencesEndpointTest.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/submission/DeleteSequencesEndpointTest.kt @@ -12,15 +12,19 @@ import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource import org.loculus.backend.api.AccessionVersion import org.loculus.backend.api.DeleteSequenceScope +import org.loculus.backend.api.DeleteSequenceScope.ALL import org.loculus.backend.api.SequenceEntryStatus import org.loculus.backend.api.Status +import org.loculus.backend.controller.ALTERNATIVE_DEFAULT_GROUP_NAME +import org.loculus.backend.controller.DEFAULT_GROUP_NAME import org.loculus.backend.controller.DEFAULT_ORGANISM +import org.loculus.backend.controller.DEFAULT_USER_NAME import org.loculus.backend.controller.EndpointTest import org.loculus.backend.controller.OTHER_ORGANISM import org.loculus.backend.controller.assertStatusIs import org.loculus.backend.controller.expectUnauthorizedResponse import org.loculus.backend.controller.generateJwtFor -import org.loculus.backend.controller.getAccessionVersions +import org.loculus.backend.controller.jwtForSuperUser import org.loculus.backend.controller.submission.SubmitFiles.DefaultFiles.NUMBER_OF_SEQUENCES import org.loculus.backend.controller.toAccessionVersion import org.loculus.backend.utils.AccessionVersionComparator @@ -40,7 +44,8 @@ class DeleteSequencesEndpointTest( fun `GIVEN invalid authorization token THEN returns 401 Unauthorized`() { expectUnauthorizedResponse(isModifyingRequest = true) { client.deleteSequenceEntries( - listOfAccessionVersionsToDelete = emptyList(), + scope = ALL, + accessionVersionsFilter = emptyList(), jwt = it, ) } @@ -61,7 +66,8 @@ class DeleteSequencesEndpointTest( ) val deletionResult = client.deleteSequenceEntries( - listOfAccessionVersionsToDelete = accessionVersionsToDelete.map { + scope = ALL, + accessionVersionsFilter = accessionVersionsToDelete.map { AccessionVersion(it.accession, it.version) }, ) @@ -95,7 +101,8 @@ class DeleteSequencesEndpointTest( ) val deletionResult = client.deleteSequenceEntries( - listOfAccessionVersionsToDelete = accessionVersionsToDelete.map { + scope = ALL, + accessionVersionsFilter = accessionVersionsToDelete.map { AccessionVersion(it.accession, it.version) }, ) @@ -127,7 +134,10 @@ class DeleteSequencesEndpointTest( val nonExistingAccession = AccessionVersion("123", 1) val nonExistingVersion = AccessionVersion("1", 123) - client.deleteSequenceEntries(listOfAccessionVersionsToDelete = listOf(nonExistingAccession, nonExistingVersion)) + client.deleteSequenceEntries( + scope = ALL, + accessionVersionsFilter = listOf(nonExistingAccession, nonExistingVersion), + ) .andExpect(status().isUnprocessableEntity) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect( @@ -148,7 +158,7 @@ class DeleteSequencesEndpointTest( hasSize(erroneousSequences.size + approvableSequences.size), ) - client.deleteSequenceEntries(scope = DeleteSequenceScope.ALL) + client.deleteSequenceEntries(scope = ALL) .andExpect(status().isOk) .andExpect(jsonPath("\$.length()").value(2 * NUMBER_OF_SEQUENCES)) @@ -212,7 +222,8 @@ class DeleteSequencesEndpointTest( val accessionVersion = convenienceClient.submitDefaultFiles(organism = DEFAULT_ORGANISM)[0] client.deleteSequenceEntries( - listOfAccessionVersionsToDelete = listOf(accessionVersion.toAccessionVersion()), + scope = ALL, + accessionVersionsFilter = listOf(accessionVersion.toAccessionVersion()), organism = OTHER_ORGANISM, ) .andExpect(status().isUnprocessableEntity) @@ -224,11 +235,12 @@ class DeleteSequencesEndpointTest( @Test fun `WHEN deleting accession versions not from the submitter THEN throws forbidden error`() { - val accessionVersions = convenienceClient.submitDefaultFiles().getAccessionVersions() + val accessionVersions = convenienceClient.submitDefaultFiles() val notSubmitter = "theOneWhoMustNotBeNamed" client.deleteSequenceEntries( - accessionVersions, + scope = ALL, + accessionVersionsFilter = accessionVersions, jwt = generateJwtFor(notSubmitter), ) .andExpect(status().isForbidden) @@ -238,6 +250,45 @@ class DeleteSequencesEndpointTest( ) } + @Test + fun `WHEN superuser deletes all entries THEN is successfully deleted`() { + val accessionVersions = convenienceClient + .submitDefaultFiles( + username = DEFAULT_USER_NAME, + groupName = DEFAULT_GROUP_NAME, + ) + + convenienceClient.submitDefaultFiles( + username = DEFAULT_USER_NAME, + groupName = ALTERNATIVE_DEFAULT_GROUP_NAME, + ) + + client.deleteSequenceEntries(scope = ALL, jwt = jwtForSuperUser) + .andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("\$.length()").value(accessionVersions.size)) + .andExpect(jsonPath("\$[0].accession").value(accessionVersions.first().accession)) + .andExpect(jsonPath("\$[0].version").value(accessionVersions.first().version)) + } + + @Test + fun `WHEN superuser deletes entries of other user THEN is successfully deleted`() { + val accessionVersions = convenienceClient.submitDefaultFiles( + username = DEFAULT_USER_NAME, + groupName = DEFAULT_GROUP_NAME, + ) + + client.deleteSequenceEntries( + scope = ALL, + accessionVersionsFilter = accessionVersions, + jwt = jwtForSuperUser, + ) + .andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("\$.length()").value(accessionVersions.size)) + .andExpect(jsonPath("\$[0].accession").value(accessionVersions.first().accession)) + .andExpect(jsonPath("\$[0].version").value(accessionVersions.first().version)) + } + @Test @Suppress("ktlint:standard:max-line-length") fun `GIVEN data with and without warnings WHEN I delete only warnings THEN only sequence with warning is deleted`() { @@ -255,7 +306,7 @@ class DeleteSequencesEndpointTest( client.deleteSequenceEntries( scope = DeleteSequenceScope.PROCESSED_WITH_WARNINGS, - listOfAccessionVersionsToDelete = listOf( + accessionVersionsFilter = listOf( AccessionVersion(accessionWithWarnings, 1), AccessionVersion(accessionOfSuccessfullyProcessedData, 1), ), @@ -268,7 +319,7 @@ class DeleteSequencesEndpointTest( not(hasItem(hasProperty("accession", `is`(accessionWithWarnings)))), ) - convenienceClient.getSequenceEntryOfUser(accession = accessionOfSuccessfullyProcessedData, version = 1) + convenienceClient.getSequenceEntry(accession = accessionOfSuccessfullyProcessedData, version = 1) .assertStatusIs(Status.AWAITING_APPROVAL) } diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/submission/ExtractUnprocessedDataEndpointTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/submission/ExtractUnprocessedDataEndpointTest.kt index fbf7b035b9..8bbcfd6000 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/submission/ExtractUnprocessedDataEndpointTest.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/submission/ExtractUnprocessedDataEndpointTest.kt @@ -106,12 +106,12 @@ class ExtractUnprocessedDataEndpointTest( val responseBody = result.expectNdjsonAndGetContent() assertThat(responseBody, hasSize(otherOrganismEntries.size)) - convenienceClient.getSequenceEntryOfUser( + convenienceClient.getSequenceEntry( accession = defaultOrganismEntries.first().accession, version = 1, organism = DEFAULT_ORGANISM, ).assertStatusIs(RECEIVED) - convenienceClient.getSequenceEntryOfUser( + convenienceClient.getSequenceEntry( accession = otherOrganismEntries.first().accession, version = 1, organism = OTHER_ORGANISM, diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/submission/GetDataToEditEndpointTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/submission/GetDataToEditEndpointTest.kt index d339a4b5fa..b3088c453e 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/submission/GetDataToEditEndpointTest.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/submission/GetDataToEditEndpointTest.kt @@ -5,12 +5,15 @@ import org.hamcrest.CoreMatchers.`is` import org.hamcrest.MatcherAssert.assertThat import org.junit.jupiter.api.Test import org.loculus.backend.api.Status +import org.loculus.backend.controller.DEFAULT_GROUP_NAME import org.loculus.backend.controller.DEFAULT_ORGANISM +import org.loculus.backend.controller.DEFAULT_USER_NAME import org.loculus.backend.controller.EndpointTest import org.loculus.backend.controller.OTHER_ORGANISM import org.loculus.backend.controller.assertStatusIs import org.loculus.backend.controller.expectUnauthorizedResponse import org.loculus.backend.controller.generateJwtFor +import org.loculus.backend.controller.jwtForSuperUser import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.MediaType import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content @@ -40,7 +43,7 @@ class GetDataToEditEndpointTest( convenienceClient.submitProcessedData(PreparedProcessedData.withErrors(firstAccession)) - convenienceClient.getSequenceEntryOfUser(accession = firstAccession, version = 1) + convenienceClient.getSequenceEntry(accession = firstAccession, version = 1) .assertStatusIs(Status.HAS_ERRORS) val editedData = convenienceClient.getSequenceEntryToEdit( @@ -139,6 +142,26 @@ class GetDataToEditEndpointTest( ) } + @Test + fun `WHEN superuser get data to edit of other user THEN is successfully get data`() { + val accessionVersion = convenienceClient + .prepareDataTo( + Status.AWAITING_APPROVAL, + username = DEFAULT_USER_NAME, + groupName = DEFAULT_GROUP_NAME, + ) + .first() + + client.getSequenceEntryToEdit( + accession = accessionVersion.accession, + version = accessionVersion.version, + jwt = jwtForSuperUser, + ) + .andExpect(status().isOk) + .andExpect(jsonPath("\$.accession").value(accessionVersion.accession)) + .andExpect(jsonPath("\$.version").value(accessionVersion.version)) + } + @Test fun `GIVEN revocation version awaiting approval THEN throws unprocessable entity error`() { val accessionVersion = convenienceClient.prepareDefaultSequenceEntriesToAwaitingApprovalForRevocation() diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/submission/GetReleasedDataEndpointTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/submission/GetReleasedDataEndpointTest.kt index 34158879a9..e20a7393b2 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/submission/GetReleasedDataEndpointTest.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/submission/GetReleasedDataEndpointTest.kt @@ -16,6 +16,7 @@ import org.loculus.backend.api.ProcessedData import org.loculus.backend.api.SiloVersionStatus import org.loculus.backend.api.Status import org.loculus.backend.controller.DEFAULT_GROUP_NAME +import org.loculus.backend.controller.DEFAULT_USER_NAME import org.loculus.backend.controller.EndpointTest import org.loculus.backend.controller.expectForbiddenResponse import org.loculus.backend.controller.expectNdjsonAndGetContent diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/submission/GetSequencesEndpointTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/submission/GetSequencesEndpointTest.kt index 7eeda44dcc..88dabd8b98 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/submission/GetSequencesEndpointTest.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/submission/GetSequencesEndpointTest.kt @@ -5,6 +5,7 @@ import org.hamcrest.Matchers.containsInAnyOrder import org.hamcrest.Matchers.containsString import org.hamcrest.Matchers.empty import org.hamcrest.Matchers.hasEntry +import org.hamcrest.Matchers.hasItem import org.hamcrest.Matchers.hasSize import org.hamcrest.Matchers.`is` import org.junit.jupiter.api.Test @@ -21,11 +22,13 @@ import org.loculus.backend.controller.ALTERNATIVE_DEFAULT_GROUP_NAME import org.loculus.backend.controller.ALTERNATIVE_DEFAULT_USER_NAME import org.loculus.backend.controller.DEFAULT_GROUP_NAME import org.loculus.backend.controller.DEFAULT_ORGANISM +import org.loculus.backend.controller.DEFAULT_USER_NAME import org.loculus.backend.controller.EndpointTest import org.loculus.backend.controller.OTHER_ORGANISM import org.loculus.backend.controller.expectUnauthorizedResponse import org.loculus.backend.controller.generateJwtFor import org.loculus.backend.controller.getAccessionVersions +import org.loculus.backend.controller.jwtForSuperUser import org.loculus.backend.controller.submission.SubmitFiles.DefaultFiles.NUMBER_OF_SEQUENCES import org.loculus.backend.utils.Accession import org.springframework.beans.factory.annotation.Autowired @@ -121,6 +124,58 @@ class GetSequencesEndpointTest( } } + @Test + fun `WHEN superuser queries data of groups THEN returns sequence entries`() { + val defaultGroupData = convenienceClient.submitDefaultFiles( + username = DEFAULT_USER_NAME, + groupName = DEFAULT_GROUP_NAME, + ) + val otherGroupData = convenienceClient.submitDefaultFiles( + username = DEFAULT_USER_NAME, + groupName = ALTERNATIVE_DEFAULT_GROUP_NAME, + ) + val accessionVersions = defaultGroupData + otherGroupData + + client.getSequenceEntries( + groupsFilter = listOf(DEFAULT_GROUP_NAME, ALTERNATIVE_DEFAULT_GROUP_NAME), + jwt = jwtForSuperUser, + ) + .andExpect(status().isOk) + .andExpect(jsonPath("\$.statusCounts.RECEIVED").value(accessionVersions.size)) + .andExpect( + jsonPath("\$.sequenceEntries.[*].accession", hasItem(defaultGroupData.first().accession)), + ) + .andExpect( + jsonPath("\$.sequenceEntries.[*].accession", hasItem(otherGroupData.first().accession)), + ) + } + + @Test + fun `WHEN superuser queries sequences without groupsFilter THEN returns sequence entries`() { + val defaultGroupData = convenienceClient.submitDefaultFiles( + username = DEFAULT_USER_NAME, + groupName = DEFAULT_GROUP_NAME, + ) + val otherGroupData = convenienceClient.submitDefaultFiles( + username = DEFAULT_USER_NAME, + groupName = ALTERNATIVE_DEFAULT_GROUP_NAME, + ) + val accessionVersions = defaultGroupData + otherGroupData + + client.getSequenceEntries( + groupsFilter = null, + jwt = jwtForSuperUser, + ) + .andExpect(status().isOk) + .andExpect(jsonPath("\$.statusCounts.RECEIVED").value(accessionVersions.size)) + .andExpect( + jsonPath("\$.sequenceEntries.[*].accession", hasItem(defaultGroupData.first().accession)), + ) + .andExpect( + jsonPath("\$.sequenceEntries.[*].accession", hasItem(otherGroupData.first().accession)), + ) + } + @Test fun `GIVEN data in many statuses WHEN querying sequences for a certain one THEN return only those sequences`() { convenienceClient.prepareDataTo(AWAITING_APPROVAL) diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/submission/ReviseEndpointTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/submission/ReviseEndpointTest.kt index ddf933629c..c81b1cd304 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/submission/ReviseEndpointTest.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/submission/ReviseEndpointTest.kt @@ -4,6 +4,7 @@ import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.hasItem import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.hasSize +import org.hamcrest.Matchers.`is` import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments @@ -14,12 +15,15 @@ import org.loculus.backend.api.Status.RECEIVED import org.loculus.backend.api.UnprocessedData import org.loculus.backend.controller.DEFAULT_GROUP_NAME import org.loculus.backend.controller.DEFAULT_ORGANISM +import org.loculus.backend.controller.DEFAULT_USER_NAME import org.loculus.backend.controller.EndpointTest import org.loculus.backend.controller.OTHER_ORGANISM +import org.loculus.backend.controller.SUPER_USER_NAME import org.loculus.backend.controller.assertStatusIs import org.loculus.backend.controller.expectNdjsonAndGetContent import org.loculus.backend.controller.expectUnauthorizedResponse import org.loculus.backend.controller.generateJwtFor +import org.loculus.backend.controller.jwtForSuperUser import org.loculus.backend.controller.submission.SubmitFiles.DefaultFiles import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.MediaType @@ -80,6 +84,28 @@ class ReviseEndpointTest( ) } + @Test + fun `WHEN superuser submits on behalf of other group THEN revised versions are created`() { + val accessions = convenienceClient + .prepareDataTo(APPROVED_FOR_RELEASE, username = DEFAULT_USER_NAME, groupName = DEFAULT_GROUP_NAME) + .map { it.accession } + + client.reviseSequenceEntries( + DefaultFiles.getRevisedMetadataFile(accessions), + DefaultFiles.sequencesFile, + jwt = jwtForSuperUser, + ) + .andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("\$.length()").value(DefaultFiles.NUMBER_OF_SEQUENCES)) + .andExpect(jsonPath("\$[0].submissionId").value("custom0")) + .andExpect(jsonPath("\$[0].accession").value(accessions.first())) + .andExpect(jsonPath("\$[0].version").value(2)) + + val sequenceEntry = convenienceClient.getSequenceEntry(accession = accessions.first(), version = 2) + assertThat(sequenceEntry.submitter, `is`(SUPER_USER_NAME)) + } + @Test fun `GIVEN entries with status 'APPROVED_FOR_RELEASE' THEN there is a revised version and returns HeaderIds`() { val accessions = convenienceClient.prepareDataTo(APPROVED_FOR_RELEASE).map { it.accession } @@ -95,9 +121,9 @@ class ReviseEndpointTest( .andExpect(jsonPath("\$[0].accession").value(accessions.first())) .andExpect(jsonPath("\$[0].version").value(2)) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 2) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 2) .assertStatusIs(RECEIVED) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(APPROVED_FOR_RELEASE) val result = client.extractUnprocessedData(DefaultFiles.NUMBER_OF_SEQUENCES) diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/submission/RevokeEndpointTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/submission/RevokeEndpointTest.kt index 91e9211003..0922deff63 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/submission/RevokeEndpointTest.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/submission/RevokeEndpointTest.kt @@ -4,12 +4,15 @@ import org.hamcrest.Matchers.containsString import org.junit.jupiter.api.Test import org.loculus.backend.api.Status import org.loculus.backend.api.Status.AWAITING_APPROVAL +import org.loculus.backend.controller.DEFAULT_GROUP_NAME import org.loculus.backend.controller.DEFAULT_ORGANISM +import org.loculus.backend.controller.DEFAULT_USER_NAME import org.loculus.backend.controller.EndpointTest import org.loculus.backend.controller.OTHER_ORGANISM import org.loculus.backend.controller.assertStatusIs import org.loculus.backend.controller.expectUnauthorizedResponse import org.loculus.backend.controller.generateJwtFor +import org.loculus.backend.controller.jwtForSuperUser import org.loculus.backend.controller.submission.SubmitFiles.DefaultFiles import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.MediaType @@ -44,7 +47,7 @@ class RevokeEndpointTest( .andExpect(jsonPath("\$[0].accession").value(accessions.first())) .andExpect(jsonPath("\$[0].version").value(2)) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 2) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 2) .assertStatusIs(AWAITING_APPROVAL) } @@ -91,6 +94,26 @@ class RevokeEndpointTest( ) } + @Test + fun `WHEN superuser revokes entries of other group THEN revocation version is created`() { + val accessions = convenienceClient + .prepareDefaultSequenceEntriesToApprovedForRelease( + username = DEFAULT_USER_NAME, + groupName = DEFAULT_GROUP_NAME, + ) + .map { it.accession } + + client.revokeSequenceEntries(accessions, jwt = jwtForSuperUser) + .andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("\$.length()").value(DefaultFiles.NUMBER_OF_SEQUENCES)) + .andExpect(jsonPath("\$[0].accession").value(accessions.first())) + .andExpect(jsonPath("\$[0].version").value(2)) + + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 2) + .assertStatusIs(AWAITING_APPROVAL) + } + @Test fun `WHEN revoking with latest version not 'APPROVED_FOR_RELEASE' THEN throws an unprocessableEntity error`() { val accessions = convenienceClient.prepareDefaultSequenceEntriesToHasErrors().map { it.accession } diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmissionControllerClient.kt b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmissionControllerClient.kt index 1a78616c43..950517a65d 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmissionControllerClient.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmissionControllerClient.kt @@ -27,8 +27,6 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post -const val DEFAULT_USER_NAME = "testuser" - class SubmissionControllerClient(private val mockMvc: MockMvc, private val objectMapper: ObjectMapper) { fun submit( metadataFile: MockMultipartFile, @@ -130,16 +128,16 @@ class SubmissionControllerClient(private val mockMvc: MockMvc, private val objec } fun approveProcessedSequenceEntries( - listOfSequencesToApprove: List? = null, + scope: ApproveDataScope, + accessionVersionsFilter: List? = null, organism: String = DEFAULT_ORGANISM, - scope: ApproveDataScope = ApproveDataScope.ALL, jwt: String? = jwtForDefaultUser, ): ResultActions = mockMvc.perform( post(addOrganismToPath("/approve-processed-data", organism = organism)) .contentType(MediaType.APPLICATION_JSON) .content( """{ - "accessionVersionsFilter": ${serialize(listOfSequencesToApprove)}, + "accessionVersionsFilter": ${serialize(accessionVersionsFilter)}, "scope": "$scope" }""", ) @@ -164,8 +162,8 @@ class SubmissionControllerClient(private val mockMvc: MockMvc, private val objec ) fun deleteSequenceEntries( - listOfAccessionVersionsToDelete: List? = null, - scope: DeleteSequenceScope = DeleteSequenceScope.ALL, + scope: DeleteSequenceScope, + accessionVersionsFilter: List? = null, organism: String = DEFAULT_ORGANISM, jwt: String? = jwtForDefaultUser, ): ResultActions = mockMvc.perform( @@ -175,7 +173,7 @@ class SubmissionControllerClient(private val mockMvc: MockMvc, private val objec .content( """ { - "accessionVersionsFilter": ${serialize(listOfAccessionVersionsToDelete)}, + "accessionVersionsFilter": ${serialize(accessionVersionsFilter)}, "scope": "$scope" } """.trimMargin(), diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmissionConvenienceClient.kt b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmissionConvenienceClient.kt index 2ff3edb6d7..99b749e46a 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmissionConvenienceClient.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmissionConvenienceClient.kt @@ -6,6 +6,7 @@ import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat import org.loculus.backend.api.AccessionVersion import org.loculus.backend.api.AccessionVersionInterface +import org.loculus.backend.api.ApproveDataScope import org.loculus.backend.api.DataUseTerms import org.loculus.backend.api.GetSequenceResponse import org.loculus.backend.api.Organism @@ -20,6 +21,7 @@ import org.loculus.backend.api.WarningsFilter import org.loculus.backend.config.BackendConfig import org.loculus.backend.controller.DEFAULT_GROUP_NAME import org.loculus.backend.controller.DEFAULT_ORGANISM +import org.loculus.backend.controller.DEFAULT_USER_NAME import org.loculus.backend.controller.OTHER_ORGANISM import org.loculus.backend.controller.expectNdjsonAndGetContent import org.loculus.backend.controller.generateJwtFor @@ -65,8 +67,10 @@ class SubmissionConvenienceClient( fun prepareDefaultSequenceEntriesToInProcessing( organism: String = DEFAULT_ORGANISM, + username: String = DEFAULT_USER_NAME, + groupName: String = DEFAULT_GROUP_NAME, ): List { - submitDefaultFiles(organism = organism) + submitDefaultFiles(organism = organism, username = username, groupName = groupName) return extractUnprocessedData(organism = organism).getAccessionVersions() } @@ -85,8 +89,13 @@ class SubmissionConvenienceClient( .andExpect(status().isNoContent) } - fun prepareDefaultSequenceEntriesToHasErrors(organism: String = DEFAULT_ORGANISM): List { - val accessionVersions = prepareDefaultSequenceEntriesToInProcessing(organism = organism) + fun prepareDefaultSequenceEntriesToHasErrors( + organism: String = DEFAULT_ORGANISM, + username: String = DEFAULT_USER_NAME, + groupName: String = DEFAULT_GROUP_NAME, + ): List { + val accessionVersions = + prepareDefaultSequenceEntriesToInProcessing(organism = organism, username = username, groupName = groupName) submitProcessedData( accessionVersions.map { PreparedProcessedData.withErrors(accession = it.accession) @@ -98,8 +107,11 @@ class SubmissionConvenienceClient( private fun prepareDefaultSequenceEntriesToAwaitingApproval( organism: String = DEFAULT_ORGANISM, + username: String = DEFAULT_USER_NAME, + groupName: String = DEFAULT_GROUP_NAME, ): List { - val accessionVersions = prepareDefaultSequenceEntriesToInProcessing(organism = organism) + val accessionVersions = + prepareDefaultSequenceEntriesToInProcessing(organism = organism, username = username, groupName = groupName) submitProcessedData( *accessionVersions.map { when (organism) { @@ -118,12 +130,19 @@ class SubmissionConvenienceClient( fun prepareDefaultSequenceEntriesToApprovedForRelease( organism: String = DEFAULT_ORGANISM, + username: String = DEFAULT_USER_NAME, + groupName: String = DEFAULT_GROUP_NAME, ): List { - val accessionVersions = prepareDefaultSequenceEntriesToAwaitingApproval(organism = organism) + val accessionVersions = prepareDefaultSequenceEntriesToAwaitingApproval( + organism = organism, + username = username, + groupName = groupName, + ) approveProcessedSequenceEntries( accessionVersions.map { AccessionVersion(it.accession, it.version) }, organism = organism, + username = username, ) return accessionVersions } @@ -140,9 +159,15 @@ class SubmissionConvenienceClient( fun prepareDefaultSequenceEntriesToAwaitingApprovalForRevocation( organism: String = DEFAULT_ORGANISM, + username: String = DEFAULT_USER_NAME, + groupName: String = DEFAULT_GROUP_NAME, ): List { - val accessionVersions = prepareDefaultSequenceEntriesToApprovedForRelease(organism = organism) - return revokeSequenceEntries(accessionVersions.map { it.accession }, organism = organism) + val accessionVersions = prepareDefaultSequenceEntriesToApprovedForRelease( + organism = organism, + username = username, + groupName = groupName, + ) + return revokeSequenceEntries(accessionVersions.map { it.accession }, organism = organism, username = username) } fun prepareRevokedSequenceEntries(organism: String = DEFAULT_ORGANISM): List { @@ -185,10 +210,10 @@ class SubmissionConvenienceClient( statusesFilter = listOf(status), ).sequenceEntries - fun getSequenceEntryOfUser(accessionVersion: AccessionVersion, userName: String = DEFAULT_USER_NAME) = - getSequenceEntryOfUser(accessionVersion.accession, accessionVersion.version, userName) + fun getSequenceEntry(accessionVersion: AccessionVersionInterface, userName: String = DEFAULT_USER_NAME) = + getSequenceEntry(accessionVersion.accession, accessionVersion.version, userName) - fun getSequenceEntryOfUser( + fun getSequenceEntry( accession: Accession, version: Long, userName: String = DEFAULT_USER_NAME, @@ -242,14 +267,17 @@ class SubmissionConvenienceClient( } fun approveProcessedSequenceEntries( - listOfSequencesToApprove: List, + accessionVersionsFilter: List, organism: String = DEFAULT_ORGANISM, + username: String = DEFAULT_USER_NAME, ): List { return deserializeJsonResponse( client .approveProcessedSequenceEntries( - listOfSequencesToApprove, + scope = ApproveDataScope.ALL, + accessionVersionsFilter = accessionVersionsFilter, organism = organism, + jwt = generateJwtFor(username), ) .andExpect(status().isOk), ) @@ -271,16 +299,47 @@ class SubmissionConvenienceClient( fun revokeSequenceEntries( listOfAccessionsToRevoke: List, organism: String = DEFAULT_ORGANISM, - ): List = - deserializeJsonResponse(client.revokeSequenceEntries(listOfAccessionsToRevoke, organism = organism)) + username: String = DEFAULT_USER_NAME, + ): List = deserializeJsonResponse( + client.revokeSequenceEntries( + listOfAccessionsToRevoke, + organism = organism, + jwt = generateJwtFor(username), + ), + ) - fun prepareDataTo(status: Status, organism: String = DEFAULT_ORGANISM): List { + fun prepareDataTo( + status: Status, + organism: String = DEFAULT_ORGANISM, + username: String = DEFAULT_USER_NAME, + groupName: String = DEFAULT_GROUP_NAME, + ): List { return when (status) { - Status.RECEIVED -> submitDefaultFiles(organism = organism) - Status.IN_PROCESSING -> prepareDefaultSequenceEntriesToInProcessing(organism = organism) - Status.HAS_ERRORS -> prepareDefaultSequenceEntriesToHasErrors(organism = organism) - Status.AWAITING_APPROVAL -> prepareDefaultSequenceEntriesToAwaitingApproval(organism = organism) - Status.APPROVED_FOR_RELEASE -> prepareDefaultSequenceEntriesToApprovedForRelease(organism = organism) + Status.RECEIVED -> submitDefaultFiles(organism = organism, username = username, groupName = groupName) + Status.IN_PROCESSING -> prepareDefaultSequenceEntriesToInProcessing( + organism = organism, + username = username, + groupName = groupName, + ) + + Status.HAS_ERRORS -> prepareDefaultSequenceEntriesToHasErrors( + organism = organism, + username = username, + groupName = groupName, + ) + + Status.AWAITING_APPROVAL -> prepareDefaultSequenceEntriesToAwaitingApproval( + organism = organism, + username = username, + groupName = groupName, + ) + + Status.APPROVED_FOR_RELEASE -> prepareDefaultSequenceEntriesToApprovedForRelease( + organism = organism, + username = username, + groupName = groupName, + ) + else -> throw Exception("Test issue: No data preparation defined for status $status") } } diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmissionJourneyTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmissionJourneyTest.kt index c3976c6590..e66ba31f5d 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmissionJourneyTest.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmissionJourneyTest.kt @@ -28,11 +28,11 @@ class SubmissionJourneyTest( fun `Submission scenario, from submission, over edit and approval ending in status 'APPROVED_FOR_RELEASE'`() { val accessions = convenienceClient.submitDefaultFiles().map { it.accession } - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(RECEIVED) convenienceClient.extractUnprocessedData() - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(IN_PROCESSING) convenienceClient.submitProcessedData( @@ -40,15 +40,15 @@ class SubmissionJourneyTest( PreparedProcessedData.withErrors(accession = it) }, ) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(HAS_ERRORS) convenienceClient.submitDefaultEditedData(accessions) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(RECEIVED) convenienceClient.extractUnprocessedData() - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(IN_PROCESSING) convenienceClient.submitProcessedData( @@ -56,7 +56,7 @@ class SubmissionJourneyTest( PreparedProcessedData.successfullyProcessed(accession = it) }, ) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(AWAITING_APPROVAL) convenienceClient.approveProcessedSequenceEntries( @@ -64,7 +64,7 @@ class SubmissionJourneyTest( AccessionVersion(it, 1) }, ) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(APPROVED_FOR_RELEASE) } @@ -73,11 +73,11 @@ class SubmissionJourneyTest( val accessions = convenienceClient.prepareDefaultSequenceEntriesToApprovedForRelease().map { it.accession } convenienceClient.reviseDefaultProcessedSequenceEntries(accessions) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 2) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 2) .assertStatusIs(RECEIVED) convenienceClient.extractUnprocessedData() - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 2) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 2) .assertStatusIs(IN_PROCESSING) convenienceClient.submitProcessedData( @@ -85,7 +85,7 @@ class SubmissionJourneyTest( PreparedProcessedData.successfullyProcessed(accession = it, version = 2) }, ) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 2) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 2) .assertStatusIs(AWAITING_APPROVAL) convenienceClient.approveProcessedSequenceEntries( @@ -93,7 +93,7 @@ class SubmissionJourneyTest( AccessionVersion(it, 2) }, ) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 2) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 2) .assertStatusIs(APPROVED_FOR_RELEASE) } diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmitEditedSequenceEntryVersionEndpointTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmitEditedSequenceEntryVersionEndpointTest.kt index 13f9f718b1..2a3c5834d4 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmitEditedSequenceEntryVersionEndpointTest.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmitEditedSequenceEntryVersionEndpointTest.kt @@ -4,11 +4,14 @@ import org.hamcrest.Matchers.containsString import org.junit.jupiter.api.Test import org.loculus.backend.api.Status import org.loculus.backend.api.UnprocessedData +import org.loculus.backend.controller.DEFAULT_GROUP_NAME +import org.loculus.backend.controller.DEFAULT_USER_NAME import org.loculus.backend.controller.EndpointTest import org.loculus.backend.controller.OTHER_ORGANISM import org.loculus.backend.controller.assertStatusIs import org.loculus.backend.controller.expectUnauthorizedResponse import org.loculus.backend.controller.generateJwtFor +import org.loculus.backend.controller.jwtForSuperUser import org.springframework.beans.factory.annotation.Autowired import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @@ -33,14 +36,14 @@ class SubmitEditedSequenceEntryVersionEndpointTest( fun `GIVEN a sequence entry has errors WHEN I submit edited data THEN the status changes to RECEIVED`() { val accessions = convenienceClient.prepareDataTo(Status.HAS_ERRORS).map { it.accession } - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(Status.HAS_ERRORS) val editedData = generateUnprocessedData(accessions.first()) client.submitEditedSequenceEntryVersion(editedData) .andExpect(status().isNoContent) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(Status.RECEIVED) } @@ -48,7 +51,7 @@ class SubmitEditedSequenceEntryVersionEndpointTest( fun `GIVEN a sequence entry is processed WHEN I submit edited data THEN the status changes to RECEIVED`() { val accessions = convenienceClient.prepareDataTo(Status.AWAITING_APPROVAL).map { it.accession } - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(Status.AWAITING_APPROVAL) val editedData = generateUnprocessedData(accessions.first()) @@ -56,7 +59,7 @@ class SubmitEditedSequenceEntryVersionEndpointTest( client.submitEditedSequenceEntryVersion(editedData) .andExpect(status().isNoContent) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(Status.RECEIVED) } @@ -64,11 +67,11 @@ class SubmitEditedSequenceEntryVersionEndpointTest( fun `WHEN a version does not exist THEN it returns an unprocessable entity error`() { val accessions = convenienceClient.prepareDataTo(Status.HAS_ERRORS).map { it.accession } - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(Status.HAS_ERRORS) val editedDataWithNonExistingVersion = generateUnprocessedData(accessions.first(), version = 2) - val sequenceString = getAccessionVersion(editedDataWithNonExistingVersion) + val sequenceString = editedDataWithNonExistingVersion.displayAccessionVersion() client.submitEditedSequenceEntryVersion(editedDataWithNonExistingVersion) .andExpect(status().isUnprocessableEntity) @@ -82,7 +85,7 @@ class SubmitEditedSequenceEntryVersionEndpointTest( fun `WHEN an accession does not exist THEN it returns an unprocessable entity error`() { val accessions = convenienceClient.prepareDataTo(Status.HAS_ERRORS).map { it.accession } - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(Status.HAS_ERRORS) val nonExistingAccession = "nonExistingAccession" @@ -97,7 +100,7 @@ class SubmitEditedSequenceEntryVersionEndpointTest( ), ) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(Status.HAS_ERRORS) } @@ -105,7 +108,7 @@ class SubmitEditedSequenceEntryVersionEndpointTest( fun `WHEN submitting data for wrong organism THEN it returns an unprocessable entity error`() { val accessions = convenienceClient.prepareDataTo(Status.HAS_ERRORS).map { it.accession } - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(Status.HAS_ERRORS) val editedData = generateUnprocessedData(accessions.first()) @@ -119,7 +122,7 @@ class SubmitEditedSequenceEntryVersionEndpointTest( ), ) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(Status.HAS_ERRORS) } @@ -127,7 +130,7 @@ class SubmitEditedSequenceEntryVersionEndpointTest( fun `WHEN a sequence entry does not belong to a user THEN it returns an forbidden error`() { val accessions = convenienceClient.prepareDataTo(Status.HAS_ERRORS).map { it.accession } - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(Status.HAS_ERRORS) val editedDataFromWrongSubmitter = generateUnprocessedData(accessions.first()) @@ -139,16 +142,27 @@ class SubmitEditedSequenceEntryVersionEndpointTest( jsonPath("\$.detail", containsString("is not a member of group")), ) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(Status.HAS_ERRORS) } + @Test + fun `WHEN superuser submits edited data for entry of other group THEN accepts data`() { + val accessionVersion = convenienceClient + .prepareDataTo(Status.HAS_ERRORS, username = DEFAULT_USER_NAME, groupName = DEFAULT_GROUP_NAME) + .first() + + val editedData = generateUnprocessedData(accessionVersion.accession, accessionVersion.version) + client.submitEditedSequenceEntryVersion(editedData, jwt = jwtForSuperUser) + .andExpect(status().isNoContent) + + convenienceClient.getSequenceEntry(accession = accessionVersion.accession, version = accessionVersion.version) + .assertStatusIs(Status.RECEIVED) + } + private fun generateUnprocessedData(accession: String, version: Long = 1) = UnprocessedData( accession = accession, version = version, data = emptyOriginalData, ) - - private fun getAccessionVersion(unprocessedData: UnprocessedData) = - "${unprocessedData.accession}.${unprocessedData.version}" } diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmitEndpointTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmitEndpointTest.kt index 15932ef372..9fd268ebda 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmitEndpointTest.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmitEndpointTest.kt @@ -15,12 +15,14 @@ import org.junit.jupiter.params.provider.MethodSource import org.loculus.backend.api.DataUseTerms import org.loculus.backend.api.Organism import org.loculus.backend.config.BackendConfig +import org.loculus.backend.controller.ALTERNATIVE_DEFAULT_GROUP_NAME import org.loculus.backend.controller.DEFAULT_GROUP_NAME import org.loculus.backend.controller.DEFAULT_ORGANISM import org.loculus.backend.controller.EndpointTest import org.loculus.backend.controller.OTHER_ORGANISM import org.loculus.backend.controller.expectUnauthorizedResponse import org.loculus.backend.controller.generateJwtFor +import org.loculus.backend.controller.jwtForSuperUser import org.loculus.backend.controller.submission.SubmitFiles.DefaultFiles import org.loculus.backend.controller.submission.SubmitFiles.DefaultFiles.NUMBER_OF_SEQUENCES import org.loculus.backend.model.SubmitModel.AcceptedFileTypes.metadataFileTypes @@ -85,6 +87,19 @@ class SubmitEndpointTest( ) } + @Test + fun `WHEN superuser submits on behalf of some group THEN is accepted`() { + submissionControllerClient.submit( + DefaultFiles.metadataFile, + DefaultFiles.sequencesFile, + jwt = jwtForSuperUser, + groupName = ALTERNATIVE_DEFAULT_GROUP_NAME, + ) + .andExpect(status().isOk) + .andExpect(content().contentType(APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("\$.length()").value(NUMBER_OF_SEQUENCES)) + } + @ParameterizedTest(name = "GIVEN {0} THEN data is accepted and submitted") @MethodSource("compressionForSubmit") fun `GIVEN valid input data THEN returns mapping of provided custom ids to generated ids`( @@ -227,8 +242,10 @@ class SubmitEndpointTest( "Bad Request", "${metadataFileTypes.displayName} has wrong extension. Must be " + ".${metadataFileTypes.validExtensions.joinToString(", .")} for uncompressed submissions or " + - ".${metadataFileTypes.getCompressedExtensions().filterKeys { it != CompressionAlgorithm.NONE } - .flatMap { it.value }.joinToString(", .")} for compressed submissions", + ".${ + metadataFileTypes.getCompressedExtensions().filterKeys { it != CompressionAlgorithm.NONE } + .flatMap { it.value }.joinToString(", .") + } for compressed submissions", DEFAULT_ORGANISM, DataUseTerms.Open, ), @@ -240,8 +257,10 @@ class SubmitEndpointTest( "Bad Request", "${sequenceFileTypes.displayName} has wrong extension. Must be " + ".${sequenceFileTypes.validExtensions.joinToString(", .")} for uncompressed submissions or " + - ".${sequenceFileTypes.getCompressedExtensions().filterKeys { it != CompressionAlgorithm.NONE } - .flatMap { it.value }.joinToString(", .")} for compressed submissions", + ".${ + sequenceFileTypes.getCompressedExtensions().filterKeys { it != CompressionAlgorithm.NONE } + .flatMap { it.value }.joinToString(", .") + } for compressed submissions", DEFAULT_ORGANISM, DataUseTerms.Open, ), diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmitProcessedDataEndpointTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmitProcessedDataEndpointTest.kt index 0cb9e1eafc..a873437f3e 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmitProcessedDataEndpointTest.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmitProcessedDataEndpointTest.kt @@ -61,7 +61,7 @@ class SubmitProcessedDataEndpointTest( ) .andExpect(status().isNoContent) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1).assertStatusIs( + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1).assertStatusIs( Status.AWAITING_APPROVAL, ) } @@ -75,7 +75,7 @@ class SubmitProcessedDataEndpointTest( ) .andExpect(status().isNoContent) - convenienceClient.getSequenceEntryOfUser(accession = accession, version = version) + convenienceClient.getSequenceEntry(accession = accession, version = version) .assertStatusIs(Status.AWAITING_APPROVAL) } @@ -107,7 +107,7 @@ class SubmitProcessedDataEndpointTest( ) .andExpect(status().isNoContent) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1).assertStatusIs( + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1).assertStatusIs( Status.AWAITING_APPROVAL, ) } @@ -130,7 +130,7 @@ class SubmitProcessedDataEndpointTest( organism = OTHER_ORGANISM, ).andExpect(status().isNoContent) - convenienceClient.getSequenceEntryOfUser( + convenienceClient.getSequenceEntry( accession = accessions.first(), version = 1, organism = OTHER_ORGANISM, @@ -176,7 +176,7 @@ class SubmitProcessedDataEndpointTest( ), ).andExpect(status().isNoContent) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1).assertStatusIs( + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1).assertStatusIs( Status.AWAITING_APPROVAL, ) @@ -206,7 +206,7 @@ class SubmitProcessedDataEndpointTest( prepareExtractedSequencesInDatabase() - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(Status.AWAITING_APPROVAL) } @@ -217,7 +217,7 @@ class SubmitProcessedDataEndpointTest( submissionControllerClient.submitProcessedData(PreparedProcessedData.withErrors(accessions.first())) .andExpect(status().isNoContent) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(Status.HAS_ERRORS) } @@ -232,7 +232,7 @@ class SubmitProcessedDataEndpointTest( ), ).andExpect(status().isNoContent) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(Status.HAS_ERRORS) } @@ -243,7 +243,7 @@ class SubmitProcessedDataEndpointTest( submissionControllerClient.submitProcessedData(PreparedProcessedData.withWarnings(accessions.first())) .andExpect(status().isNoContent) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(Status.AWAITING_APPROVAL) } @@ -261,7 +261,7 @@ class SubmitProcessedDataEndpointTest( .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(jsonPath("\$.detail").value(invalidDataScenario.expectedErrorMessage)) - val sequenceStatus = convenienceClient.getSequenceEntryOfUser( + val sequenceStatus = convenienceClient.getSequenceEntry( accession = accessions.first(), version = 1, ) @@ -282,7 +282,7 @@ class SubmitProcessedDataEndpointTest( .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(jsonPath("\$.detail").value("Accession version $nonExistentAccession.1 does not exist")) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1).assertStatusIs( + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1).assertStatusIs( Status.IN_PROCESSING, ) } @@ -306,7 +306,7 @@ class SubmitProcessedDataEndpointTest( ), ) - convenienceClient.getSequenceEntryOfUser(accession = accessions.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(Status.IN_PROCESSING) } @@ -315,7 +315,7 @@ class SubmitProcessedDataEndpointTest( val accessionsNotInProcessing = convenienceClient.prepareDataTo(Status.AWAITING_APPROVAL).map { it.accession } val accessionsInProcessing = convenienceClient.prepareDataTo(Status.IN_PROCESSING).map { it.accession } - convenienceClient.getSequenceEntryOfUser(accession = accessionsNotInProcessing.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessionsNotInProcessing.first(), version = 1) .assertStatusIs(Status.AWAITING_APPROVAL) submissionControllerClient.submitProcessedData( @@ -331,9 +331,9 @@ class SubmitProcessedDataEndpointTest( ), ) - convenienceClient.getSequenceEntryOfUser(accession = accessionsInProcessing.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessionsInProcessing.first(), version = 1) .assertStatusIs(Status.IN_PROCESSING) - convenienceClient.getSequenceEntryOfUser(accession = accessionsNotInProcessing.first(), version = 1) + convenienceClient.getSequenceEntry(accession = accessionsNotInProcessing.first(), version = 1) .assertStatusIs(Status.AWAITING_APPROVAL) } diff --git a/get_testuser_token.sh b/get_testuser_token.sh index 107a2b70e3..64e3a9842c 100755 --- a/get_testuser_token.sh +++ b/get_testuser_token.sh @@ -4,8 +4,10 @@ set -eu KEYCLOAK_TOKEN_URL="http://localhost:8083/realms/loculus/protocol/openid-connect/token" KEYCLOAK_CLIENT_ID="test-cli" -echo "Retrieving JWT from $KEYCLOAK_TOKEN_URL" -jwt_keycloak=$(curl -X POST "$KEYCLOAK_TOKEN_URL" --fail-with-body -H 'Content-Type: application/x-www-form-urlencoded' -d "username=testuser&password=testuser&grant_type=password&client_id=$KEYCLOAK_CLIENT_ID") +usernameAndPassword="${1:-testuser}" + +echo "Retrieving JWT from $KEYCLOAK_TOKEN_URL for user $usernameAndPassword" +jwt_keycloak=$(curl -X POST "$KEYCLOAK_TOKEN_URL" --fail-with-body -H 'Content-Type: application/x-www-form-urlencoded' -d "username=$usernameAndPassword&password=$usernameAndPassword&grant_type=password&client_id=$KEYCLOAK_CLIENT_ID") jwt=$(echo "$jwt_keycloak" | jq -r '.access_token') if [ -z "$jwt" ]; then @@ -14,4 +16,4 @@ if [ -z "$jwt" ]; then fi echo "JWT retrieved successfully:" echo -echo "$jwt" \ No newline at end of file +echo "$jwt" diff --git a/kubernetes/loculus/templates/keycloak-config-map.yaml b/kubernetes/loculus/templates/keycloak-config-map.yaml index eb324a68f3..9f4ef29936 100644 --- a/kubernetes/loculus/templates/keycloak-config-map.yaml +++ b/kubernetes/loculus/templates/keycloak-config-map.yaml @@ -35,7 +35,7 @@ data: { "username": "testuser_{{$index}}_{{$browser}}", "enabled": true, - "email": "testuser_{{$index}}_{{$browser}}@keycloak.org", + "email": "testuser_{{$index}}_{{$browser}}@void.o", "emailVerified": true, "firstName": "{{$index}}_{{$browser}}", "lastName": "TestUser", @@ -63,7 +63,7 @@ data: { "username": "testuser", "enabled": true, - "email": "testuser@keycloak.org", + "email": "testuser@void.o", "emailVerified" : true, "firstName": "Test", "lastName": "User", @@ -89,7 +89,7 @@ data: { "username": "insdc_ingest_user", "enabled": true, - "email": "insdc_ingest_user@keycloak.org", + "email": "insdc_ingest_user@void.o", "emailVerified" : true, "firstName": "INSDC Ingest", "lastName": "User", @@ -115,7 +115,7 @@ data: { "username": "dummy_preprocessing_pipeline", "enabled": true, - "email": "dummy_preprocessing_pipeline@keycloak.org", + "email": "dummy_preprocessing_pipeline@void.o", "emailVerified" : true, "firstName": "Dummy", "lastName": "Preprocessing", @@ -138,10 +138,36 @@ data: ] } }, + { + "username": "superuser", + "enabled": true, + "email": "superuser@void.o", + "emailVerified" : true, + "firstName": "Dummy", + "lastName": "SuperUser", + "credentials": [ + { + "type": "password", + "value": "superuser" + } + ], + "realmRoles": [ + "super_user", + "offline_access" + ], + "attributes": { + "university": "University of Test" + }, + "clientRoles": { + "account": [ + "manage-account" + ] + } + }, { "username": "silo_import_job", "enabled": true, - "email": "silo_import_job@keycloak.org", + "email": "silo_import_job@void.o", "emailVerified": true, "firstName": "SILO", "lastName": "ImportJob", @@ -207,6 +233,10 @@ data: { "name": "get_released_data", "description": "Privileges for getting released data" + }, + { + "name": "super_user", + "description": "Privileges for curators to modify sequence entries of any user" } ] }, diff --git a/website/src/components/SequenceDetailsPage/SequencesDetailsPage.astro b/website/src/components/SequenceDetailsPage/SequencesDetailsPage.astro index 912765a4b5..62ac8ffa28 100644 --- a/website/src/components/SequenceDetailsPage/SequencesDetailsPage.astro +++ b/website/src/components/SequenceDetailsPage/SequencesDetailsPage.astro @@ -7,7 +7,6 @@ import { getReferenceGenomes, getRuntimeConfig } from '../../config'; import { routes } from '../../routes/routes.ts'; import { GroupManagementClient } from '../../services/groupManagementClient'; import { type DataUseTermsHistoryEntry } from '../../types/backend'; -import { type Group } from '../../types/backend.ts'; import { getAccessToken } from '../../utils/getAccessToken'; import ErrorBox from '../common/ErrorBox.astro'; import MdiEye from '~icons/mdi/eye'; @@ -34,14 +33,12 @@ const clientConfig = getRuntimeConfig().public; const session = Astro.locals.session; const accessToken = getAccessToken(session); -const groupsResult = - accessToken !== undefined ? await GroupManagementClient.create().getGroupsOfUser(accessToken) : undefined; - const isMyGroup = accessToken !== undefined && - groupsResult !== undefined && - groupsResult.isOk() && - groupsResult.value.some((myGroupItem: Group) => myGroupItem.groupName === group); + (await GroupManagementClient.create().getGroupsOfUser(accessToken)).match( + (groups) => groups.some((myGroupItem) => myGroupItem.groupName === group), + () => false, + ); --- {