From e4b382024d86ab180d26745c2d709064ba66ef91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0est=C3=A1k=20V=C3=ADt?= Date: Tue, 31 Jan 2017 09:31:21 +0100 Subject: [PATCH] Added API for listing of scans Added API support --- app/controllers/Projects.scala | 7 +++- app/controllers/Statistics.scala | 34 +++++++++++++++++-- app/controllers/api/ApiApplication.scala | 16 +++++++++ app/controllers/api/ApiConfig.scala | 9 +++++ app/controllers/api/ApiController.scala | 30 ++++++++++++++++ app/controllers/api/ApiResource.scala | 4 +++ app/controllers/api/ApiResources.scala | 11 ++++++ .../api/AuthenticatedApiApplication.scala | 5 +++ app/modules/ConfigModule.scala | 32 ++++++++++++++++- build.sbt | 2 +- 10 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 app/controllers/api/ApiApplication.scala create mode 100644 app/controllers/api/ApiConfig.scala create mode 100644 app/controllers/api/ApiController.scala create mode 100644 app/controllers/api/ApiResource.scala create mode 100644 app/controllers/api/ApiResources.scala create mode 100644 app/controllers/api/AuthenticatedApiApplication.scala diff --git a/app/controllers/Projects.scala b/app/controllers/Projects.scala index bf245c3..501aab4 100644 --- a/app/controllers/Projects.scala +++ b/app/controllers/Projects.scala @@ -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 + } diff --git a/app/controllers/Statistics.scala b/app/controllers/Statistics.scala index 26e26b8..a2b74b2 100644 --- a/app/controllers/Statistics.scala +++ b/app/controllers/Statistics.scala @@ -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 => diff --git a/app/controllers/api/ApiApplication.scala b/app/controllers/api/ApiApplication.scala new file mode 100644 index 0000000..d666195 --- /dev/null +++ b/app/controllers/api/ApiApplication.scala @@ -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 + } + } +} diff --git a/app/controllers/api/ApiConfig.scala b/app/controllers/api/ApiConfig.scala new file mode 100644 index 0000000..53f8a21 --- /dev/null +++ b/app/controllers/api/ApiConfig.scala @@ -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 +} diff --git a/app/controllers/api/ApiController.scala b/app/controllers/api/ApiController.scala new file mode 100644 index 0000000..98797c0 --- /dev/null +++ b/app/controllers/api/ApiController.scala @@ -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)."))) + } + } + } + +} diff --git a/app/controllers/api/ApiResource.scala b/app/controllers/api/ApiResource.scala new file mode 100644 index 0000000..1ee725e --- /dev/null +++ b/app/controllers/api/ApiResource.scala @@ -0,0 +1,4 @@ +package controllers.api + +final case class ApiResource private[api](name: String) extends AnyVal + diff --git a/app/controllers/api/ApiResources.scala b/app/controllers/api/ApiResources.scala new file mode 100644 index 0000000..3cbc762 --- /dev/null +++ b/app/controllers/api/ApiResources.scala @@ -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) +} \ No newline at end of file diff --git a/app/controllers/api/AuthenticatedApiApplication.scala b/app/controllers/api/AuthenticatedApiApplication.scala new file mode 100644 index 0000000..2c5a345 --- /dev/null +++ b/app/controllers/api/AuthenticatedApiApplication.scala @@ -0,0 +1,5 @@ +package controllers.api + +class AuthenticatedApiApplication(resources: Set[ApiResource]) { + def isAllowed(resource: ApiResource): Boolean = resources contains resource +} diff --git a/app/modules/ConfigModule.scala b/app/modules/ConfigModule.scala index 951039d..0567485 100644 --- a/app/modules/ConfigModule.scala +++ b/app/modules/ConfigModule.scala @@ -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)))) ++ diff --git a/build.sbt b/build.sbt index c6f159c..d9433c0 100644 --- a/build.sbt +++ b/build.sbt @@ -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"