backend/app/utils/controller/FailureToResultMapper.scala (147 lines of code) (raw):
package utils.controller
import com.amazonaws.services.cloudwatch.AmazonCloudWatchClientBuilder
import com.amazonaws.services.cloudwatch.model.{Dimension, MetricDatum, PutMetricDataRequest}
import com.amazonaws.util.EC2MetadataUtils
import net.logstash.logback.marker.Markers.{aggregate, append}
import play.api.http.HeaderNames
import play.api.libs.json.JsError
import play.api.mvc.{Result, Results}
import services.{AWSDiscoveryConfig, Metrics, MetricsService}
import utils.attempt._
import utils.auth.User
import utils.{AwsCredentials, Logging}
import java.util.Date
import scala.jdk.CollectionConverters._
import scala.language.higherKinds
import scala.util.control.NonFatal
trait FailureToResultMapper {
def failureToResult(err: Failure, user: Option[User] = None): Result
}
object FailureToResultMapper extends Logging {
def failureToResult(err: Failure, user: Option[User] = None): Result = {
def logUserAndMessage(user: Option[User], errorString: String) =
user match {
case Some(user) => logger.error(user.asLogMarker, errorString)
case None => logger.error(errorString)
}
err match {
case hidden : HiddenFailure =>
// don't leak the reason that the sensitive failure occurred - log it locally and return an ambiguous error
logger.error(s"${hidden.msg}: ${hidden.actualMessage}", hidden.cause.orNull)
Results.Unauthorized(hidden.msg)
case MisconfiguredAccount(msg) =>
logUserAndMessage(user, s"Misconfigured account: $msg")
Results.Forbidden(msg)
case SecondFactorRequired(msg) =>
logUserAndMessage(user, s"Unauthorised, second factor auth required: $msg")
Results.Unauthorized(msg).withHeaders(HeaderNames.WWW_AUTHENTICATE -> "Pfi2fa")
case PanDomainCookieInvalid(msg, _) =>
logUserAndMessage(user, s"Pan domain login failure: $msg")
Results.Unauthorized(msg).withHeaders(HeaderNames.WWW_AUTHENTICATE -> "Panda")
case ClientFailure(msg) =>
logUserAndMessage(user, s"Bad request: $msg")
Results.BadRequest(msg)
case NotFoundFailure(msg) =>
logUserAndMessage(user, s"Not found: $msg")
Results.NotFound(msg)
case UnsupportedOperationFailure(msg) =>
logUserAndMessage(user, s"Unsupported Operation: $msg")
Results.BadRequest(msg)
case JsonParseFailure(errors) =>
logUserAndMessage(user, s"Json parse failure: $errors")
Results.BadRequest(JsError.toJson(errors))
case IllegalStateFailure(msg) =>
logUserAndMessage(user, s"Illegal state failure: $msg")
Results.InternalServerError(msg)
case ElasticSearchQueryFailure(throwable, responseCode, maybeResponseBody) =>
val userMarker = user.map(_.asLogMarker)
val responseMarker = maybeResponseBody.map(append("elasticsearchResponse", _))
val markers = aggregate((userMarker ++ responseMarker).toList.asJava)
logger.error(markers, "Elasticsearch query failure", throwable)
maybeResponseBody match {
case Some(responseBody) =>
// Since we got a response body from elasticsearch, pass it straight through to client
Results.Status(responseCode)(responseBody).as("application/json")
case None =>
Results.Status(responseCode)(throwable.toString)
}
case TransactionFailure(msg) =>
logUserAndMessage(user, s"Transaction failure: $msg")
Results.InternalServerError(msg)
case AlreadySetupFailure(msg) =>
logUserAndMessage(user, s"Already setup failure: $msg")
Results.Conflict(msg)
case neo4j: Neo4JFailure =>
logUserAndMessage(user, s"Neo4J Failure ${neo4j.msg}")
Results.InternalServerError(neo4j.msg)
case neo4j: Neo4JTransientFailure =>
logUserAndMessage(user, s"Neo4J Transient Failure ${neo4j.msg}")
Results.InternalServerError(neo4j.msg)
case neo4j: Neo4JValueFailure =>
logUserAndMessage(user, s"Neo4J Value Failure ${neo4j.msg}")
Results.InternalServerError(neo4j.msg)
case aws: AwsSdkFailure =>
logUserAndMessage(user, s"AWS Sdk Failure: ${aws.msg}")
Results.InternalServerError(aws.msg)
case unknown: UnknownFailure =>
logUserAndMessage(user, s"Unknown Failure: ${unknown.msg}")
Results.InternalServerError(unknown.msg)
case externalTranscriptionOutputFailure: ExternalTranscriptionOutputFailure =>
logUserAndMessage(user, s"externalTranscriptionOutputFailure Failure: ${externalTranscriptionOutputFailure.msg}")
Results.InternalServerError(externalTranscriptionOutputFailure.msg)
case documentUpdateFailure: DocumentUpdateFailure =>
logUserAndMessage(user, s"documentUpdateFailure Failure: ${documentUpdateFailure.msg}")
Results.InternalServerError(documentUpdateFailure.msg)
case MissingPermissionFailure(msg) =>
logUserAndMessage(user, s"Missing Permission Failure: $msg")
Results.Forbidden(msg)
case MultipleFailures(failures) => failureToResult(failures.head, user)
case PreviewNotSupportedFailure =>
logUserAndMessage(user, s"Preview Supported Failure")
Results.NotAcceptable
case SubprocessInterruptedFailure =>
logUserAndMessage(user, s"Subprocess Interrupted Failure: ${SubprocessInterruptedFailure.msg}")
Results.InternalServerError(SubprocessInterruptedFailure.msg)
case ContentTooLongFailure(msg) =>
logUserAndMessage(user, s"Content Too Long Failure: ${msg}")
Results.EntityTooLarge(msg)
case DeleteFailure(msg) =>
logUserAndMessage(user, s"Delete failed: ${msg}")
Results.InternalServerError(msg)
case WorkspaceCopyFailure(msg) =>
logUserAndMessage(user, s"Workspace copy failed: ${msg}")
Results.InternalServerError(msg)
case DeleteNotAllowed(msg) =>
logUserAndMessage(user, s"Deletion is refused: ${msg}")
Results.Forbidden(msg)
case OcrTimeout(msg) =>
logger.error(msg)
Results.InternalServerError(msg)
case f: PostgresWriteFailure =>
logger.error(f.msg, f.throwable)
Results.InternalServerError(f.msg)
case f: PostgresReadFailure =>
logger.error(f.msg, f.throwable)
Results.InternalServerError(f.msg)
case FfMpegFailure(error, message) =>
logger.error(message)
Results.InternalServerError(message)
}
}
}
class DefaultFailureToResultMapper extends FailureToResultMapper with Logging {
final override def failureToResult(err: Failure, user: Option[User]): Result = {
FailureToResultMapper.failureToResult(err, user)
}
}
class CloudWatchReportingFailureToResultMapper(metricsService: MetricsService) extends FailureToResultMapper with Logging {
override def failureToResult(err: Failure, user: Option[User]): Result = err match {
// Don't alarm on expected authentication failures such as expired tokens.
// We also use a 401 to indicate to clients that the user exists but they have to present their 2fa code
case PanDomainCookieInvalid(_, false) |
AuthenticationFailure(_, _, false) |
SecondFactorRequired(_) |
// Not found can occur during normal usage, for example clicking the path to an ingestion you can't see
// from a file within that ingestion that is shared with you via a workspace
NotFoundFailure(_) |
// We expect this to happen if a worker is terminated midway through an OCR job. Another worker will pick it up
SubprocessInterruptedFailure =>
FailureToResultMapper.failureToResult(err, user)
case _ =>
metricsService.updateMetric(Metrics.failureToResultMapper)
FailureToResultMapper.failureToResult(err, user)
}
}