Added API for listing of scans

Added API support
This commit is contained in:
Šesták Vít
2017-01-31 09:31:21 +01:00
parent cd37dda90c
commit e4b382024d
10 changed files with 144 additions and 6 deletions

View File

@@ -3,7 +3,7 @@ package controllers
class Projects (
val projectMap: Map[String, String],
private val teamLeaders: Map[TeamId, String],
private val projectToTeams: Map[String, Set[TeamId]]
val projectToTeams: Map[String, Set[TeamId]]
) {
val projectSet: Set[String] = projectMap.keySet
@@ -24,4 +24,9 @@ class Projects (
def teamSet: Set[Team] = teamsById.values.toSet
def teamsByProjectId(projectId: String): Set[TeamId] = projectToTeams.filter{case (projectSpecification, _) =>
val projectName = projectMap(projectId)
(projectSpecification == projectName) || (projectSpecification startsWith s"$projectName:")
}.values.flatten.toSet
}

View File

@@ -6,16 +6,23 @@ import com.google.inject.name.Named
import com.ysoft.odc.statistics.{LibDepStatistics, TagStatistics}
import com.ysoft.odc.{ArtifactFile, ArtifactItem}
import controllers.DependencyCheckReportsParser.ResultWithSelection
import models.{ExportedVulnerability, LibraryTag}
import controllers.api.{ApiConfig, ApiController}
import models.LibraryTag
import org.joda.time.DateTime
import play.api.i18n.MessagesApi
import play.api.libs.json._
import play.twirl.api.Txt
import services._
import views.html.DefaultRequest
import scala.concurrent.{ExecutionContext, Future}
class Statistics @Inject() (
final case class ScannedRepository(url: String, branch: String)
final case class ScannedProject(name: String, repos: Seq[ScannedRepository], projects: Seq[String], key: String)
//noinspection TypeAnnotation
class Statistics @Inject()(
reportsParser: DependencyCheckReportsParser,
reportsProcessor: DependencyCheckReportsProcessor,
projectReportsProvider: ProjectReportsProvider,
@@ -28,8 +35,9 @@ class Statistics @Inject() (
projects: Projects,
vulnerabilityNotificationService: VulnerabilityNotificationService,
issueTrackerServiceOption: Option[IssueTrackerService],
protected val apiConfig: ApiConfig,
val env: AuthEnv
)(implicit val messagesApi: MessagesApi, executionContext: ExecutionContext) extends AuthenticatedController {
)(implicit val messagesApi: MessagesApi, executionContext: ExecutionContext) extends AuthenticatedController with ApiController {
private val versions = Map[String, Int]()
@@ -194,6 +202,26 @@ class Statistics @Inject() (
}
}
implicit val scannedRepositoryFormat = Json.format[ScannedRepository]
implicit val scannedProjectFormats = Json.format[ScannedProject]
def table() = ApiAction(ProjectTable).async{
val RepoFetch = """.*Fetching 'refs/heads/(.*)' from '(.*)'\..*""".r // Bamboo does not seem to have a suitable API, so we are parsing it from logs…
val (lastRefreshTime, resultsFuture) = projectReportsProvider.resultsForVersions(versions)
resultsFuture map { allResults =>
val t = projects.projectMap
val rows = t.toIndexedSeq.sortBy(r => (r._2.toLowerCase, r._2)).map{case (key, name) =>
val repos = allResults._1.get(key).map(_._3.dataString.lines.collect{
case RepoFetch(branch, repo) => ScannedRepository(repo, branch)
}.toSet).getOrElse(Set.empty).toIndexedSeq.sortBy(ScannedRepository.unapply)
ScannedProject(name, repos, projects.teamsByProjectId(key).toIndexedSeq.map(_.name).sorted, key)
}
Ok(Json.toJson(rows))
}
}
def vulnerableLibraries(selectorOption: Option[String]) = ReadAction.async { implicit req =>
val (lastRefreshTime, resultsFuture) = projectReportsProvider.resultsForVersions(versions)
resultsFuture flatMap { allResults =>

View File

@@ -0,0 +1,16 @@
package controllers.api
import play.api.libs.Crypto
sealed abstract class ApiApplication {
def authenticate(appToken: String): Option[AuthenticatedApiApplication]
}
object ApiApplication{
final class Plain(token: String, authenticatedApiApplication: AuthenticatedApiApplication) extends ApiApplication{
override def authenticate(appToken: String): Option[AuthenticatedApiApplication] = {
if(Crypto.constantTimeEquals(appToken, token)) Some(authenticatedApiApplication)
else None
}
}
}

View File

@@ -0,0 +1,9 @@
package controllers.api
class ApiConfig(applications: Map[String, ApiApplication]){
def getApplication(appName: String, appToken: String): Option[AuthenticatedApiApplication] = for{
app <- applications.get(appName)
authenticatedApp <- app.authenticate(appToken)
} yield authenticatedApp
}

View File

@@ -0,0 +1,30 @@
package controllers.api
import controllers.AuthenticatedController
import play.api.mvc.{ActionBuilder, Request, Result}
import play.twirl.api.Txt
import scala.concurrent.Future
trait ApiController extends AuthenticatedController with ApiResources {
protected def apiConfig: ApiConfig
protected def ApiAction(resource: ApiResource) = new ActionBuilder[Request] {
override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]): Future[Result] = {
val appNameOption = request.headers.get("x-app-name").orElse(request.getQueryString("app-name"))
val appTokenOption = request.headers.get("x-app-token").orElse(request.getQueryString("app-token"))
(appNameOption, appTokenOption) match {
case (Some(appName), Some(appToken)) =>
apiConfig.getApplication(appName, appToken) match {
case Some(app) =>
if(app.isAllowed(resource)) block(request)
else Future.successful(Unauthorized(Txt("The application is not allowed to access "+resource.name)))
case None => Future.successful(Unauthorized(Txt("Unknown application or bad token")))
}
case _ => Future.successful(Unauthorized(Txt("Missing auth headers x-app-name and x-app-token (or similar GET parameters).")))
}
}
}
}

View File

@@ -0,0 +1,4 @@
package controllers.api
final case class ApiResource private[api](name: String) extends AnyVal

View File

@@ -0,0 +1,11 @@
package controllers.api
trait ApiResources {
val ProjectTable = ApiResource("project-table")
}
object ApiResources extends ApiResources{
val All = Set(ProjectTable)
private val AllByName = All.map(res => res.name -> res).toMap
def byName(name: String): Option[ApiResource] = AllByName.get(name)
}

View File

@@ -0,0 +1,5 @@
package controllers.api
class AuthenticatedApiApplication(resources: Set[ApiResource]) {
def isAllowed(resource: ApiResource): Boolean = resources contains resource
}

View File

@@ -6,7 +6,9 @@ import java.nio.file.{Files, Path, Paths}
import java.util.concurrent.Executors
import akka.util.ClassLoaderObjectInputStream
import com.typesafe.config.{Config, ConfigObject, ConfigValue}
import com.ysoft.odc._
import controllers.api._
import controllers.{MissingGavExclusions, Projects, TeamId, WarningSeverity}
import net.ceedubs.ficus.Ficus._
import net.ceedubs.ficus.readers.ArbitraryTypeReader._
@@ -114,6 +116,33 @@ class ConfigModule extends Module {
)
}
private def parseApiApplication(value: Config): ApiApplication = {
import scala.collection.JavaConversions._
val authenticatedApiApplication = new AuthenticatedApiApplication(
value.getStringList("resources").map(resName =>
ApiResources.byName(resName).getOrElse(sys.error(s"unknown resource $resName"))
).toSet)
value.getString("tokenType") match {
case "plain" => new ApiApplication.Plain(value.getString("token"), authenticatedApiApplication)
}
}
private def parseApiConfig(configuration: Configuration): ApiConfig = {
import scala.collection.JavaConversions._
new ApiConfig(
configuration.getObject("yssdc.api.apps") match {
case None => Map.empty[String, ApiApplication]
case Some(obj) => Map(
(
for{
(key, value) <- obj
} yield key -> parseApiApplication(value.asInstanceOf[ConfigObject].toConfig)
).toSeq: _*
)
}
)
}
override def bindings(environment: Environment, configuration: Configuration): Seq[Binding[_]] = Seq(
bind[String].qualifiedWith("bamboo-server-url").toInstance(configuration.getString("yssdc.bamboo.url").getOrElse(sys.error("Key yssdc.bamboo.url is not set"))),
configuration.getString("yssdc.reports.provider") match{
@@ -126,7 +155,8 @@ class ConfigModule extends Module {
),
bind[ExecutionContext].qualifiedWith("email-sending").toInstance(ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor())),
bind[LogSmellChecks].qualifiedWith("log-smells").toInstance(LogSmellChecks(configuration.underlying.getAs[Map[String, LogSmell]]("yssdc.logSmells").getOrElse(Map()))),
bind[Projects].to(parseProjects(configuration))
bind[Projects].to(parseProjects(configuration)),
bind[ApiConfig].to(parseApiConfig(configuration))
) ++
configuration.underlying.getAs[Absolutizer]("app").map(a => bind[Absolutizer].toInstance(a)) ++
configuration.getString("play.cache.path").map(cachePath => bind[CacheApi].toInstance(new FileCacheApi(Paths.get(cachePath)))) ++

View File

@@ -73,7 +73,7 @@ libraryDependencies += "org.webjars.bower" % "jquery.scrollTo" % "2.1.2"
libraryDependencies += "net.codingwell" %% "scala-guice" % "4.0.0"
libraryDependencies += "com.iheart" %% "ficus" % "1.2.3"
libraryDependencies += "com.iheart" %% "ficus" % "1.4.0"
libraryDependencies += "org.owasp" % "dependency-check-core" % "1.4.2"