Skip to content

Commit

Permalink
feat(backend): record important activities in audit log table (#1518)
Browse files Browse the repository at this point in the history
  • Loading branch information
chaoran-chen authored Mar 31, 2024
1 parent b8aec2c commit bad7279
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 1 deletion.
14 changes: 14 additions & 0 deletions backend/src/main/kotlin/org/loculus/backend/log/AuditLogTable.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.loculus.backend.log

import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.datetime

const val AUDIT_LOG_TABLE_NAME = "audit_log"

object AuditLogTable : Table(AUDIT_LOG_TABLE_NAME) {

val idColumn = long("id")
val usernameColumn = text("username").nullable()
val timestampColumn = datetime("timestamp")
val descriptionColumn = text("description")
}
23 changes: 23 additions & 0 deletions backend/src/main/kotlin/org/loculus/backend/log/AuditLogger.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.loculus.backend.log

import org.jetbrains.exposed.sql.insert
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional

@Component
@Transactional
class AuditLogger {

fun log(description: String) {
AuditLogTable.insert {
it[descriptionColumn] = description
}
}

fun log(username: String, description: String) {
AuditLogTable.insert {
it[usernameColumn] = username
it[descriptionColumn] = description
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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.log.AuditLogger
import org.loculus.backend.model.UNIQUE_CONSTRAINT_VIOLATION_SQL_STATE
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
Expand All @@ -23,6 +24,7 @@ import org.springframework.transaction.annotation.Transactional
@Transactional
class GroupManagementDatabaseService(
private val groupManagementPreconditionValidator: GroupManagementPreconditionValidator,
private val auditLogger: AuditLogger,
) {

fun getDetailsOfGroup(groupName: String): GroupDetails {
Expand Down Expand Up @@ -78,6 +80,8 @@ class GroupManagementDatabaseService(
it[userNameColumn] = authenticatedUser.username
it[groupNameColumn] = group.groupName
}

auditLogger.log(authenticatedUser.username, "Created group: ${group.groupName}")
}

fun getGroupsOfUser(authenticatedUser: AuthenticatedUser): List<Group> {
Expand Down Expand Up @@ -122,6 +126,7 @@ class GroupManagementDatabaseService(
it[userNameColumn] = usernameToAdd
it[groupNameColumn] = groupName
}
auditLogger.log(authenticatedUser.username, "Added $usernameToAdd to group $groupName")
} catch (e: ExposedSQLException) {
if (e.sqlState == UNIQUE_CONSTRAINT_VIOLATION_SQL_STATE) {
throw ConflictException(
Expand All @@ -139,6 +144,7 @@ class GroupManagementDatabaseService(
(userNameColumn eq usernameToRemove) and
(groupNameColumn eq groupName)
}
auditLogger.log(authenticatedUser.username, "Removed $usernameToRemove from group $groupName")
}

fun getAllGroups(): List<Group> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import org.loculus.backend.auth.AuthenticatedUser
import org.loculus.backend.controller.BadRequestException
import org.loculus.backend.controller.ProcessingValidationException
import org.loculus.backend.controller.UnprocessableEntityException
import org.loculus.backend.log.AuditLogger
import org.loculus.backend.service.datauseterms.DataUseTermsTable
import org.loculus.backend.service.groupmanagement.GroupManagementDatabaseService
import org.loculus.backend.service.groupmanagement.GroupManagementPreconditionValidator
Expand All @@ -77,6 +78,7 @@ class SubmissionDatabaseService(
pool: DataSource,
private val emptyProcessedDataProvider: EmptyProcessedDataProvider,
private val compressionService: CompressionService,
private val auditLogger: AuditLogger,
) {

init {
Expand Down Expand Up @@ -160,15 +162,22 @@ class SubmissionDatabaseService(
log.info { "updating processed data" }
val reader = BufferedReader(InputStreamReader(inputStream))

val accessionVersions = mutableListOf<String>()
reader.lineSequence().forEach { line ->
val submittedProcessedData = try {
objectMapper.readValue<SubmittedProcessedData>(line)
} catch (e: JacksonException) {
throw BadRequestException("Failed to deserialize NDJSON line: ${e.message}", e)
}
accessionVersions.add(submittedProcessedData.displayAccessionVersion())

insertProcessedDataWithStatus(submittedProcessedData, organism, pipelineVersion)
}

auditLogger.log(
"<pipeline version $pipelineVersion>",
"Processed ${accessionVersions.size} sequences: ${accessionVersions.joinToString()}",
)
}

private fun insertProcessedDataWithStatus(
Expand Down Expand Up @@ -309,6 +318,10 @@ class SubmissionDatabaseService(
.select { statusCondition and accessionCondition and scopeCondition }
.map { AccessionVersion(it[SequenceEntriesView.accessionColumn], it[SequenceEntriesView.versionColumn]) }

if (accessionVersionsToUpdate.isEmpty()) {
return emptyList()
}

for (accessionVersionsChunk in accessionVersionsToUpdate.chunked(1000)) {
SequenceEntriesTable.update(
where = {
Expand All @@ -320,6 +333,12 @@ class SubmissionDatabaseService(
}
}

auditLogger.log(
authenticatedUser.username,
"Approved ${accessionVersionsToUpdate.size} sequences: " +
accessionVersionsToUpdate.joinToString { it.displayAccessionVersion() },
)

return accessionVersionsToUpdate
}

Expand Down Expand Up @@ -559,6 +578,12 @@ class SubmissionDatabaseService(
),
)

auditLogger.log(
authenticatedUser.username,
"Revoked ${accessions.size} sequences: " +
accessions.joinToString(),
)

return SequenceEntriesView
.slice(
SequenceEntriesView.accessionColumn,
Expand Down Expand Up @@ -645,6 +670,12 @@ class SubmissionDatabaseService(
SequenceEntriesTable.deleteWhere { accessionVersionIsIn(accessionVersionsChunk) }
}

auditLogger.log(
authenticatedUser.username,
"Delete ${sequenceEntriesToDelete.size} " +
"unreleased sequences: " + sequenceEntriesToDelete.joinToString { it.displayAccessionVersion() },
)

return sequenceEntriesToDelete
}

Expand All @@ -665,6 +696,12 @@ class SubmissionDatabaseService(
SequenceEntriesPreprocessedDataTable.deleteWhere {
accessionVersionEquals(editedAccessionVersion)
}

auditLogger.log(
authenticatedUser.username,
"Edited sequence: " +
editedAccessionVersion.displayAccessionVersion(),
)
}

fun getSequenceEntryVersionToEdit(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ 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.log.AuditLogger
import org.loculus.backend.model.SubmissionId
import org.loculus.backend.model.SubmissionParams
import org.loculus.backend.service.GenerateAccessionFromNumberService
Expand Down Expand Up @@ -49,6 +50,7 @@ class UploadDatabaseService(
private val accessionPreconditionValidator: AccessionPreconditionValidator,
private val dataUseTermsDatabaseService: DataUseTermsDatabaseService,
private val generateAccessionFromNumberService: GenerateAccessionFromNumberService,
private val auditLogger: AuditLogger,
) {

fun batchInsertMetadataInAuxTable(
Expand Down Expand Up @@ -186,6 +188,12 @@ class UploadDatabaseService(
)
}

auditLogger.log(
submissionParams.authenticatedUser.username,
"Submitted or revised ${insertionResult.size} sequences: " +
insertionResult.joinToString { it.displayAccessionVersion() },
)

return@transaction insertionResult
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.loculus.backend.service.submission

import org.loculus.backend.log.AuditLogger
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
import java.util.concurrent.TimeUnit
Expand All @@ -9,13 +10,16 @@ private val log = mu.KotlinLogging.logger {}
@Component
class UseNewerProcessingPipelineVersionTask(
private val submissionDatabaseService: SubmissionDatabaseService,
private val auditLogger: AuditLogger,
) {

@Scheduled(fixedDelay = 10, timeUnit = TimeUnit.SECONDS)
fun task() {
val newVersion = submissionDatabaseService.useNewerProcessingPipelineIfPossible()
if (newVersion != null) {
log.info { "Started using results from new processing pipeline: version $newVersion" }
val logMessage = "Started using results from new processing pipeline: version $newVersion"
log.info(logMessage)
auditLogger.log(logMessage)
}
}
}
7 changes: 7 additions & 0 deletions backend/src/main/resources/db/migration/V1__init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,10 @@ create table seqset_to_records (
references seqsets(seqset_id, seqset_version)
on delete cascade
);

create table audit_log (
id bigserial primary key,
username text,
timestamp timestamp not null default now(),
description text not null
);

0 comments on commit bad7279

Please sign in to comment.