mirror of
https://github.com/ysoftdevs/odc-analyzer.git
synced 2026-01-11 22:41:31 +01:00
162 lines
8.0 KiB
Scala
162 lines
8.0 KiB
Scala
package services
|
||
|
||
import _root_.org.joda.time.DateTime
|
||
import com.google.inject.Inject
|
||
import com.mohiva.play.silhouette.api.LoginInfo
|
||
import com.ysoft.odc.SetDiff
|
||
import controllers.{ProjectsWithReports, ReportInfo}
|
||
import models._
|
||
import models.tables._
|
||
import play.api.Logger
|
||
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
|
||
import slick.dbio.FutureAction
|
||
import slick.jdbc.TransactionIsolation
|
||
|
||
import scala.concurrent.{ExecutionContext, Future}
|
||
|
||
final class SingleFutureExecutionThrottler() (implicit executionContext: ExecutionContext){
|
||
private var nextFuture: Future[_] = Future.successful(null)
|
||
|
||
def throttle[T](f: => Future[T]): Future[T] = synchronized{
|
||
val newFuture = nextFuture.recover{ case _ => null}.flatMap(_ => f)
|
||
nextFuture = newFuture
|
||
newFuture
|
||
}
|
||
|
||
}
|
||
|
||
final class NoThrottler() (implicit executionContext: ExecutionContext){
|
||
def throttle[T](f: => Future[T]): Future[T] = f
|
||
}
|
||
|
||
class VulnerabilityNotificationService @Inject() (protected val dbConfigProvider: DatabaseConfigProvider)(implicit executionContext: ExecutionContext) extends HasDatabaseConfigProvider[models.profile.type]{
|
||
import dbConfig.driver.api._
|
||
import models.tables._
|
||
|
||
def subscribers = db.run(vulnerabilitySubscriptions.result).map(_.groupBy(_.user))
|
||
def watchedProjectsByUser(identity: LoginInfo) = db.run(vulnerabilitySubscriptions.filter(_.user === identity).result)
|
||
|
||
def subscribe(user: LoginInfo, project: String) = db.run(
|
||
DBIO.seq(
|
||
ensureUserHasNotificationDigestStatus(user),
|
||
vulnerabilitySubscriptions += VulnerabilitySubscription(user = user, project = project)
|
||
)
|
||
)
|
||
|
||
private def ensureUserHasNotificationDigestStatus(user: LoginInfo): DBIOAction[Unit, slick.dbio.NoStream, Effect.Read with Effect.Write] = (
|
||
notificationDigestStatuses.filter(_.user === user).result.map(_.nonEmpty) flatMap {
|
||
case true => DBIO.seq()
|
||
case false => for{
|
||
changelogTopIdOption <- changelogTopIdOptionQuery
|
||
(res: Int) <- notificationDigestStatuses += NotificationDigestStatus(user = user, lastChangelogIdOption = changelogTopIdOption)
|
||
} yield ()
|
||
}
|
||
).withTransactionIsolation(TransactionIsolation.Serializable)
|
||
|
||
def unsubscribe(user: LoginInfo, project: String) = db.run(vulnerabilitySubscriptions.filter(vs => vs.user === user && vs.project === project).delete)
|
||
|
||
def getRecipientsForProjects(projects: Set[ReportInfo]) = {
|
||
val bareProjects = projects.map(_.bare)
|
||
val expandedProjects = projects ++ bareProjects
|
||
val relevantFullIds = expandedProjects.map(_.fullId)
|
||
db.run(vulnerabilitySubscriptions.filter(_.project inSet relevantFullIds).map(_.user()).result)
|
||
}
|
||
|
||
class ExportPlatform[T, U] private[VulnerabilityNotificationService] (ept: ExportPlatformTables[T, U]) {
|
||
def changeProjects(ticketId: Int, diff: SetDiff[String], projects: ProjectsWithReports) = db.run(
|
||
DBIO.seq(
|
||
ept.projects.filter(_.exportedVulnerabilityId === ticketId).delete,
|
||
ept.projects ++= diff.newSet.map(fullId => ExportedVulnerabilityProject(ticketId, fullId)).toSet
|
||
).transactionally
|
||
)
|
||
|
||
def projectsForTickets(ticketsIds: Set[Int]): Future[Map[Int, Set[String]]] = db.run(
|
||
ept.projects.filter(_.exportedVulnerabilityId inSet ticketsIds).result
|
||
).map{_.groupBy(_.exportedVulnerabilityId).mapValues(_.map(_.projectFullId).toSet).map(identity).withDefaultValue(Set())}
|
||
|
||
def ticketsForVulnerabilities(vulnerabilities: Traversable[String]) = db.run(
|
||
ept.tickets.filter(_.vulnerabilityName inSet vulnerabilities).result
|
||
).map(_.map{ rec =>
|
||
rec._2.vulnerabilityName -> rec
|
||
}.toMap)
|
||
|
||
def ticketForVulnerability(vulnerabilityName: String) = db.run(
|
||
ept.tickets.filter(_.vulnerabilityName === vulnerabilityName).map(_.base).result
|
||
).map(_.headOption)
|
||
|
||
|
||
def addTicket(vulnerabilityTicket: ExportedVulnerability[T], projects: Set[ReportInfo]): Future[Any] = db.run(
|
||
(
|
||
ept.tickets.map(_.base).returning(ept.tickets.map(_.id)) += vulnerabilityTicket
|
||
).flatMap( id =>
|
||
ept.projects ++= projects.map(ri => ExportedVulnerabilityProject(id, ri.fullId)).toSet
|
||
).transactionally
|
||
)
|
||
|
||
}
|
||
|
||
/**
|
||
* The changelogThrottler is a temporary hack than prevents some congestion that seems to occur in HikariCP or maybe in Slick.
|
||
* It prevents exceptions like “java.sql.SQLException: Timeout of 1001ms encountered waiting for connection”.
|
||
* It is probably prevented at a wrong level, but it works :)
|
||
*/
|
||
private val changelogThrottler = new SingleFutureExecutionThrottler()
|
||
// private val changelogThrottler = new NoThrottler()
|
||
|
||
def changeAffectedProjects(vulnerabilityName: String, affectedProjectsDiff: SetDiff[String]): Future[Unit] = {
|
||
val time = DateTime.now()
|
||
def createRecord(projectName: String, direction: Change.Direction) = Change(
|
||
time = time,
|
||
vulnerabilityName = vulnerabilityName,
|
||
projectName = projectName,
|
||
direction = direction,
|
||
notifiedToSomebody = false
|
||
)
|
||
val recordsToAdd = affectedProjectsDiff.added.map(projectName => createRecord(projectName, Change.Direction.Added)) ++
|
||
affectedProjectsDiff.removed.map(projectName => createRecord(projectName, Change.Direction.Removed))
|
||
/*
|
||
Transaction:
|
||
It is essential to ensure that records in changelog appear in order. Low level of isolation might be even worse than running outside of transaction.
|
||
In longer term, it should be wrapped with transaction *with a proper isolation level* together with the export status modification.
|
||
*/
|
||
changelogThrottler.throttle(db.run(
|
||
(changelog.map(_.base) ++= recordsToAdd).withTransactionIsolation(TransactionIsolation.Serializable)
|
||
).map(_ => ()))
|
||
}
|
||
|
||
private def changelogTopIdOptionQuery = changelog.map(_.id).max.result
|
||
|
||
def sendDigestToSubscriber(subscriber: LoginInfo, subscriptions: Seq[VulnerabilitySubscription])(sendDigest: Seq[Change] => Future[Unit]): Future[Unit] = {
|
||
def subscriptionCondition(change: Changes, subscription: VulnerabilitySubscription): Rep[Boolean] = {
|
||
//noinspection ScalaUnnecessaryParentheses – because it looks less confusing
|
||
(change.projectName === subscription.project) ||
|
||
(if (subscription.project contains '/') (false: Rep[Boolean]) else change.projectName.startsWith(subscription.project + "/"))
|
||
}
|
||
val projectCondition: Changes => Rep[Boolean] = (change) =>
|
||
subscriptions.foldLeft(false: Rep[Boolean])((cond, subscription) => cond || subscriptionCondition(change, subscription))
|
||
val notificationDigestStatusSelection = notificationDigestStatuses.filter(_.user === subscriber)
|
||
db.run(
|
||
(
|
||
for{
|
||
oldStatus <- notificationDigestStatusSelection.result.map(_.head)
|
||
_ = println(oldStatus.lastChangelogIdOption.fold(changelog.filter(_ => true: Rep[Boolean]))(lastChangelogId => changelog.filter(_.id > lastChangelogId)).filter(projectCondition).result.statements.mkString("\n"))
|
||
changelogSinceLastNotified <- oldStatus.lastChangelogIdOption.fold(changelog.filter(_ => true: Rep[Boolean]))(lastChangelogId => changelog.filter(_.id > lastChangelogId)).filter(projectCondition).result
|
||
changelogIds = changelogSinceLastNotified.map(_._1)
|
||
changelogTopIdOption <- changelogTopIdOptionQuery
|
||
newLastChangelogIdOption = changelogTopIdOption.orElse(oldStatus.lastChangelogIdOption)
|
||
(_: Unit) <- FutureAction(sendDigest(changelogSinceLastNotified.map(_._2))) // Yes, we are waiting for user to be notified over some slow I/O when having an open transaction
|
||
(_: Int) <- notificationDigestStatusSelection.map(_.lastChangelogId).update(newLastChangelogIdOption)
|
||
(_: Int) <- changelog.filter(_.id inSet changelogIds).map(_.notifiedToSomebody).update(true)
|
||
} yield ()
|
||
).withTransactionIsolation(TransactionIsolation.Serializable)
|
||
)
|
||
}
|
||
|
||
val issueTrackerExport = new ExportPlatform(tables.issueTrackerExportTables)
|
||
|
||
val mailExport = new ExportPlatform(tables.mailExportTables)
|
||
|
||
val diffDbExport = new ExportPlatform(tables.diffDbExportTables)
|
||
|
||
}
|