Skip to content

Commit

Permalink
feat: Add feature flag to disable lastModificationDate check (DEV-3870)…
Browse files Browse the repository at this point in the history
… (#3313)
  • Loading branch information
seakayone authored Jul 11, 2024
1 parent da1d61d commit 9c1a2c5
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 71 deletions.
3 changes: 3 additions & 0 deletions webapi/src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -456,5 +456,8 @@ app {
features {
allow-erase-projects = false
allow-erase-projects = ${?ALLOW_ERASE_PROJECTS}

disable-last-modification-date-check = false
disable-last-modification-date-check = ${?DISABLE_LAST_MODIFICATION_DATE_CHECK}
}
}
22 changes: 15 additions & 7 deletions webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,10 @@ final case class InstrumentationServerConfig(
interval: Duration,
)

final case class Features(allowEraseProjects: Boolean)
final case class Features(
allowEraseProjects: Boolean,
disableLastModificationDateCheck: Boolean,
)

object AppConfig {
type AppConfigurationsTest = AppConfig & DspIngestConfig & Triplestore & Features & Sipi
Expand All @@ -182,14 +185,19 @@ object AppConfig {
val parseConfig: UIO[AppConfig] = {
val descriptor = deriveConfig[AppConfig].mapKey(toKebabCase)
val source = TypesafeConfigProvider.fromTypesafeConfig(ConfigFactory.load().getConfig("app").resolve)
read(descriptor from source).orDie
read(descriptor from source).tap(logFeaturesEnabled).orDie
}

val layer: ULayer[AppConfigurations] = {
val appConfigLayer = ZLayer.fromZIO(
parseConfig.tap(c => ZIO.logInfo("Feature: ALLOW_ERASE_PROJECTS enabled").when(c.features.allowEraseProjects)),
)
projectAppConfigurations(appConfigLayer).tap(_ => ZIO.logInfo(">>> AppConfig Initialized <<<"))
val layer: ULayer[AppConfigurations] =
projectAppConfigurations(ZLayer.fromZIO(parseConfig))
.tap(_ => ZIO.logInfo(">>> AppConfig Initialized <<<"))

private def logFeaturesEnabled(c: AppConfig) = {
val features = List(
"ALLOW_ERASE_PROJECTS" -> c.features.allowEraseProjects,
"DISABLE_LAST_MODIFICATION_DATE_CHECK" -> c.features.disableLastModificationDateCheck,
).collect { case (feature, enabled) if enabled => feature }
ZIO.logInfo(s"Features enabled: ${features.mkString(", ")}").when(features.nonEmpty)
}

def projectAppConfigurations[R](appConfigLayer: URLayer[R, AppConfig]): URLayer[R, AppConfigurations] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,37 @@ final case class ResourcesResponderV2(
def createResource(createResource: CreateResourceRequestV2): Task[ReadResourcesSequenceV2] =
createHandler(createResource)

/**
* If resource has already been modified, make sure that its lastModificationDate is given in the request body.
* It is a conflict if the resource has been modified since the client last read it.
* It is also conflict if the resource has a lastModificationDate but it was not provided by the client.
*
* @param resource The resource to be updated.
* @param providedLastModificationDate The lastModificationDate provided by the client.
* @return Fails with an [[EditConflictException]] if there is a conflict.
*/
private def ensureNoConflictingChange(
resource: ReadResourceV2,
providedLastModificationDate: Option[Instant],
): IO[EditConflictException, Unit] = {
val existingLastModificationDate = resource.lastModificationDate
val isConflict = (
for {
existingDate <- existingLastModificationDate
providedDate <- providedLastModificationDate
} yield existingDate != providedDate
).getOrElse(existingLastModificationDate.nonEmpty)
ZIO
.fail(
EditConflictException(
s"Resource ${resource.resourceIri} has been modified since you last read it. Its lastModificationDate " +
s"${existingLastModificationDate.map(_.toString).getOrElse("")} must be included in the request body.",
),
)
.when(isConflict && !appConfig.features.disableLastModificationDateCheck)
.unit
}

/**
* Updates a resources metadata.
*
Expand All @@ -201,7 +232,7 @@ final case class ResourcesResponderV2(
private def updateResourceMetadataV2(
updateResourceMetadataRequestV2: UpdateResourceMetadataRequestV2,
): Task[UpdateResourceMetadataResponseV2] = {
def makeTaskFuture: Task[UpdateResourceMetadataResponseV2] = {
def makeTaskFuture: Task[UpdateResourceMetadataResponseV2] =
for {
// Get the metadata of the resource to be updated.
resourcesSeq <- getResourcePreviewV2(
Expand All @@ -220,25 +251,7 @@ final case class ResourcesResponderV2(
ZIO.fail(BadRequestException(msg))
}

// If resource has already been modified, make sure that its lastModificationDate is given in the request body.
_ <-
ZIO.when(
resource.lastModificationDate.nonEmpty && updateResourceMetadataRequestV2.maybeLastModificationDate.isEmpty,
) {
val msg =
s"Resource <${resource.resourceIri}> has been modified in the past. Its lastModificationDate " +
s"${resource.lastModificationDate.get} must be included in the request body."
ZIO.fail(EditConflictException(msg))
}

// Make sure that the resource hasn't been updated since the client got its last modification date.
_ <- ZIO.when(
updateResourceMetadataRequestV2.maybeLastModificationDate.nonEmpty &&
resource.lastModificationDate != updateResourceMetadataRequestV2.maybeLastModificationDate,
) {
val msg = s"Resource <${resource.resourceIri}> has been modified since you last read it"
ZIO.fail(EditConflictException(msg))
}
_ <- ensureNoConflictingChange(resource, updateResourceMetadataRequestV2.maybeLastModificationDate)

// Check that the user has permission to modify the resource.
_ <- resourceUtilV2.checkResourcePermission(
Expand Down Expand Up @@ -318,7 +331,6 @@ final case class ResourcesResponderV2(
maybePermissions = updateResourceMetadataRequestV2.maybePermissions,
lastModificationDate = newModificationDate,
)
}

for {
// Do the remaining pre-update checks and the update while holding an update lock on the resource.
Expand Down Expand Up @@ -370,11 +382,7 @@ final case class ResourcesResponderV2(
ZIO.fail(BadRequestException(msg))
}

// Make sure that the resource hasn't been updated since the client got its last modification date.
_ <- ZIO.when(resource.lastModificationDate != deleteResourceV2.maybeLastModificationDate) {
val msg = s"Resource <${resource.resourceIri}> has been modified since you last read it"
ZIO.fail(EditConflictException(msg))
}
_ <- ensureNoConflictingChange(resource, deleteResourceV2.maybeLastModificationDate)

// If a custom delete date was provided, make sure it's later than the resource's most recent timestamp.
_ <- ZIO.when(
Expand Down Expand Up @@ -462,11 +470,7 @@ final case class ResourcesResponderV2(
ZIO.fail(BadRequestException(msg))
}

// Make sure that the resource hasn't been updated since the client got its last modification date.
_ <- ZIO.when(resource.lastModificationDate != eraseResourceV2.maybeLastModificationDate) {
val msg = s"Resource <${resource.resourceIri}> has been modified since you last read it"
ZIO.fail(EditConflictException(msg))
}
_ <- ensureNoConflictingChange(resource, eraseResourceV2.maybeLastModificationDate)

// Check that the resource is not referred to by any other resources. We ignore rdf:subject (so we
// can erase the resource's own links) and rdf:object (in case there is a deleted link value that
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import scala.collection.immutable

import dsp.errors.*
import org.knora.webapi.*
import org.knora.webapi.config.Features
import org.knora.webapi.messages.IriConversions.*
import org.knora.webapi.messages.OntologyConstants
import org.knora.webapi.messages.SmartIri
Expand Down Expand Up @@ -87,6 +88,7 @@ trait OntologyTriplestoreHelpers {
}

final case class OntologyTriplestoreHelpersLive(
features: Features,
triplestore: TriplestoreService,
stringFormatter: StringFormatter,
) extends OntologyTriplestoreHelpers {
Expand Down Expand Up @@ -211,50 +213,52 @@ final case class OntologyTriplestoreHelpersLive(
}

/**
* Checks that the last modification date of an ontology is the same as the one we expect it to be.
* Checks that:
* 1. The metadata of an ontology is loaded and has a `lastModificationDate`.
* 2. The `lastModificationDate` of an ontology is the same as the one we expect it to be.
*
* @param internalOntologyIri the internal IRI of the ontology.
* @param expectedLastModificationDate the last modification date that the ontology is expected to have.
* @param errorFun a function that throws an exception. It will be called if the expected last modification date is not found.
* @return a failed Future if the expected last modification date is not found.
* @param ontologyIri The internal IRI of the ontology.
* @param expectedLastModificationDate The last modification date that the ontology is expected to have.
* @param error It will be returned if the expected last modification date is not found.
* @return A failed Task if the checks fail:
* The 1. check fails with an [[InconsistentRepositoryDataException]] and
* The 2. check fails with the provided error.
*/
private def checkOntologyLastModificationDate(
internalOntologyIri: SmartIri,
ontologyIri: SmartIri,
expectedLastModificationDate: Instant,
errorFun: => Nothing,
disableLastModificationDateCheck: Boolean,
error: => Exception,
): Task[Unit] =
for {
existingOntologyMetadata <- loadOntologyMetadata(internalOntologyIri)

_ = existingOntologyMetadata match {
case Some(metadata) =>
metadata.lastModificationDate match {
case Some(lastModificationDate) =>
if (lastModificationDate != expectedLastModificationDate) {
errorFun
}

case None =>
throw InconsistentRepositoryDataException(
s"Ontology $internalOntologyIri has no ${OntologyConstants.KnoraBase.LastModificationDate}",
)
}

case None =>
throw NotFoundException(
s"Ontology $internalOntologyIri (corresponding to ${internalOntologyIri.toOntologySchema(ApiV2Complex)}) not found",
)
metadataMaybe <- loadOntologyMetadata(ontologyIri)
externalOntologyIri <- ZIO.attempt(ontologyIri.toOntologySchema(ApiV2Complex))
metadata <-
ZIO
.fromOption(metadataMaybe)
.orElseFail(NotFoundException(s"Ontology $externalOntologyIri not found."))
existingLastModificationDate <-
ZIO
.fromOption(metadata.lastModificationDate)
.orElseFail {
val msg = s"Ontology $externalOntologyIri has no ${OntologyConstants.KnoraBase.LastModificationDate}."
InconsistentRepositoryDataException(msg)
}
_ <-
ZIO
.fail(error)
.when(!disableLastModificationDateCheck && existingLastModificationDate != expectedLastModificationDate)
} yield ()

override def checkOntologyLastModificationDateBeforeUpdate(
internalOntologyIri: SmartIri,
expectedLastModificationDate: Instant,
): Task[Unit] =
checkOntologyLastModificationDate(
internalOntologyIri = internalOntologyIri,
expectedLastModificationDate = expectedLastModificationDate,
errorFun = throw EditConflictException(
internalOntologyIri,
expectedLastModificationDate,
features.disableLastModificationDateCheck,
EditConflictException(
s"Ontology ${internalOntologyIri.toOntologySchema(ApiV2Complex)} has been modified by another user, please reload it and try again.",
),
)
Expand All @@ -264,9 +268,10 @@ final case class OntologyTriplestoreHelpersLive(
expectedLastModificationDate: Instant,
): Task[Unit] =
checkOntologyLastModificationDate(
internalOntologyIri = internalOntologyIri,
expectedLastModificationDate = expectedLastModificationDate,
errorFun = throw UpdateNotPerformedException(
internalOntologyIri,
expectedLastModificationDate,
features.disableLastModificationDateCheck,
UpdateNotPerformedException(
s"Ontology ${internalOntologyIri.toOntologySchema(ApiV2Complex)} was not updated. Please report this as a possible bug.",
),
)
Expand Down

0 comments on commit 9c1a2c5

Please sign in to comment.