Skip to content

Commit

Permalink
APIS-6625 - Correct signing after loss of box and keys (#105)
Browse files Browse the repository at this point in the history
  • Loading branch information
AndySpaven authored Jan 16, 2024
1 parent 4c6fe8c commit 8202c9c
Show file tree
Hide file tree
Showing 41 changed files with 612 additions and 402 deletions.
7 changes: 4 additions & 3 deletions app/Module.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import com.google.inject.AbstractModule
import io.swagger.v3.parser.OpenAPIV3Parser
import io.swagger.v3.parser.core.extensions.SwaggerParserExtension

import uk.gov.hmrc.play.bootstrap.http.{DefaultHttpClient, HttpClient}
import uk.gov.hmrc.http.HttpClient
import uk.gov.hmrc.play.bootstrap.http.DefaultHttpClient
import uk.gov.hmrc.ramltools.loaders.{RamlLoader, UrlRewriter}

import uk.gov.hmrc.apipublisher.connectors.MicroserviceConnector._
import uk.gov.hmrc.apipublisher.connectors.OASFileLoader.{MicroserviceOASFileLocator, OASFileLocator}
import uk.gov.hmrc.apipublisher.connectors.{DocumentationRamlLoader, DocumentationUrlRewriter}
import uk.gov.hmrc.apipublisher.services.{OasParserImpl, OasVersionDefinitionService}

Expand All @@ -31,8 +32,8 @@ class Module extends AbstractModule {
bind(classOf[UrlRewriter]).to(classOf[DocumentationUrlRewriter])
bind(classOf[RamlLoader]).to(classOf[DocumentationRamlLoader])
bind(classOf[HttpClient]).to(classOf[DefaultHttpClient])
bind(classOf[OASFileLocator]).toInstance(MicroserviceOASFileLocator)
bind(classOf[SwaggerParserExtension]).toInstance(new OpenAPIV3Parser)
bind(classOf[OASFileLocator]).toInstance(MicroserviceOASFileLocator)
bind(classOf[OasVersionDefinitionService.OasParser]).toInstance(new OasParserImpl)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@
* limitations under the License.
*/

package uk.gov.hmrc.apipublisher.wiring
package uk.gov.hmrc.apipublisher.config

import javax.inject.Inject

import play.api.{Configuration, Environment}
import uk.gov.hmrc.play.bootstrap.config.ServicesConfig

class AppContext @Inject() (val runModeConfiguration: Configuration, environment: Environment, servicesConfig: ServicesConfig) {
class AppConfig @Inject() (val runModeConfiguration: Configuration, environment: Environment, servicesConfig: ServicesConfig) {

lazy val appName = runModeConfiguration.getOptional[String]("appName").getOrElse(throw new RuntimeException("appName is not configured"))
lazy val appUrl = runModeConfiguration.getOptional[String]("appUrl").getOrElse(throw new RuntimeException("appUrl is not configured"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import javax.inject.{Inject, Provider, Singleton}
import scala.concurrent.duration.FiniteDuration

import play.api.inject.{Binding, Module}
import play.api.{Configuration, Environment, Mode}
import play.api.{Configuration, Environment}
import uk.gov.hmrc.play.bootstrap.config.ServicesConfig

import uk.gov.hmrc.apipublisher.connectors._
Expand All @@ -42,8 +42,6 @@ class ConfigurationModule extends Module {
class ApiDefinitionConfigProvider @Inject() (val runModeConfiguration: Configuration, environment: Environment, servicesConfig: ServicesConfig)
extends Provider[ApiDefinitionConfig] {

protected def mode: Mode = environment.mode

override def get(): ApiDefinitionConfig = {
val serviceBaseUrl = servicesConfig.baseUrl("api-definition")
ApiDefinitionConfig(serviceBaseUrl)
Expand All @@ -54,8 +52,6 @@ class ApiDefinitionConfigProvider @Inject() (val runModeConfiguration: Configura
class ApiScopeConfigProvider @Inject() (val runModeConfiguration: Configuration, environment: Environment, servicesConfig: ServicesConfig)
extends Provider[ApiScopeConfig] {

protected def mode: Mode = environment.mode

override def get(): ApiScopeConfig = {
val serviceBaseUrl = servicesConfig.baseUrl("api-scope")
ApiScopeConfig(serviceBaseUrl)
Expand All @@ -66,8 +62,6 @@ class ApiScopeConfigProvider @Inject() (val runModeConfiguration: Configuration,
class ApiSSubscriptionFieldsConfigProvider @Inject() (val runModeConfiguration: Configuration, environment: Environment, servicesConfig: ServicesConfig)
extends Provider[ApiSSubscriptionFieldsConfig] {

protected def mode: Mode = environment.mode

override def get(): ApiSSubscriptionFieldsConfig = {
val serviceBaseUrl = servicesConfig.baseUrl("api-subscription-fields")
ApiSSubscriptionFieldsConfig(serviceBaseUrl)
Expand All @@ -78,8 +72,6 @@ class ApiSSubscriptionFieldsConfigProvider @Inject() (val runModeConfiguration:
class MicroserviceConnectorConfigProvider @Inject() (val runModeConfiguration: Configuration, environment: Environment, servicesConfig: ServicesConfig)
extends Provider[MicroserviceConnector.Config] {

protected def mode: Mode = environment.mode

override def get(): MicroserviceConnector.Config = {
val validateApiDefinition = runModeConfiguration.getOptional[Boolean]("validateApiDefinition").getOrElse(true)
val oasParserMaxDuration = FiniteDuration(runModeConfiguration.getMillis("oasParserMaxDuration"), TimeUnit.MILLISECONDS)
Expand Down
10 changes: 7 additions & 3 deletions app/uk/gov/hmrc/apipublisher/connectors/ConnectorRecovery.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@

package uk.gov.hmrc.apipublisher.connectors

import scala.concurrent.Future

import play.api.http.Status.UNPROCESSABLE_ENTITY
import uk.gov.hmrc.http.{UnprocessableEntityException, UpstreamErrorResponse}
import play.api.mvc.Result
import play.api.mvc.Results.UnprocessableEntity
import uk.gov.hmrc.http.UpstreamErrorResponse

trait ConnectorRecovery {

def unprocessableRecovery[U]: PartialFunction[Throwable, U] = {
case UpstreamErrorResponse(message, UNPROCESSABLE_ENTITY, _, _) => throw new UnprocessableEntityException(message)
def unprocessableRecovery[U]: PartialFunction[Throwable, Future[Either[Result, U]]] = {
case UpstreamErrorResponse(message, UNPROCESSABLE_ENTITY, _, _) => Future.successful(Left(UnprocessableEntity(message)))
}
}
125 changes: 41 additions & 84 deletions app/uk/gov/hmrc/apipublisher/connectors/MicroserviceConnector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,17 @@

package uk.gov.hmrc.apipublisher.connectors

import java.io.{FileNotFoundException, InputStream}
import java.io.InputStream
import java.nio.charset.StandardCharsets.UTF_8
import javax.inject.{Inject, Singleton}
import scala.concurrent.duration.FiniteDuration
import scala.concurrent.{ExecutionContext, Future, blocking}
import scala.jdk.CollectionConverters._
import scala.util.Try
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}

import akka.actor.ActorSystem
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.parser.core.extensions.SwaggerParserExtension
import io.swagger.v3.parser.core.models.ParseOptions
import org.apache.commons.io.IOUtils
import org.everit.json.schema.Schema
import org.everit.json.schema.loader.SchemaLoader
import org.everit.json.schema.{Schema, ValidationException}
import org.json.JSONObject

import play.api.Environment
Expand All @@ -42,34 +38,21 @@ import uk.gov.hmrc.ramltools.RAML
import uk.gov.hmrc.ramltools.loaders.RamlLoader

import uk.gov.hmrc.apipublisher.models.APICategory.{OTHER, categoryMap}
import uk.gov.hmrc.apipublisher.models.{ApiAndScopes, ServiceLocation}
import uk.gov.hmrc.apipublisher.models._
import uk.gov.hmrc.apipublisher.util.ApplicationLogger

object MicroserviceConnector {

trait OASFileLocator {
def locationOf(serviceLocation: ServiceLocation, version: String): String
}

object MicroserviceOASFileLocator extends OASFileLocator {

def locationOf(serviceLocation: ServiceLocation, version: String): String =
s"${serviceLocation.serviceUrl}/api/conf/$version/application.yaml"
}

case class Config(validateApiDefinition: Boolean, oasParserMaxDuration: FiniteDuration)
}

@Singleton
class MicroserviceConnector @Inject() (
config: MicroserviceConnector.Config,
ramlLoader: RamlLoader,
oasFileLocator: MicroserviceConnector.OASFileLocator,
openAPIV3Parser: SwaggerParserExtension,
oasFileLoader: OASFileLoader,
http: HttpClient,
env: Environment
)(implicit val ec: ExecutionContext,
system: ActorSystem
)(implicit val ec: ExecutionContext
) extends ConnectorRecovery with HttpReadsOption with ApplicationLogger {

val apiDefinitionSchema: Schema = {
Expand All @@ -88,32 +71,46 @@ class MicroserviceConnector @Inject() (
}
}

def getAPIAndScopes(serviceLocation: ServiceLocation)(implicit hc: HeaderCarrier): Future[Option[ApiAndScopes]] = {
def getAPIAndScopes(serviceLocation: ServiceLocation)(implicit hc: HeaderCarrier): Future[Either[PublishError, ApiAndScopes]] = {
import play.api.http.Status.{NOT_FOUND, UNPROCESSABLE_ENTITY}

import uk.gov.hmrc.http.UpstreamErrorResponse

val url = s"${serviceLocation.serviceUrl}/api/definition"

http.GET[Option[ApiAndScopes]](url)(readOptionOfNotFound, implicitly, implicitly)
.map(validateApiAndScopesAgainstSchema)
.map(defaultCategories)
.recover(unprocessableRecovery)
.map {
_.toRight(DefinitionFileNoBodyReturned(serviceLocation))
}
.recover {
case UpstreamErrorResponse(_, NOT_FOUND, _, _) => Left(DefinitionFileNotFound(serviceLocation))
case UpstreamErrorResponse(message, UNPROCESSABLE_ENTITY, _, _) => Left(DefinitionFileUnprocessableEntity(serviceLocation, message))
}
.map(_.map(defaultCategories))
.map(_.flatMap(validateApiAndScopesAgainstSchema))
}

private def validateApiAndScopesAgainstSchema(apiAndScopes: Option[ApiAndScopes]): Option[ApiAndScopes] = {
apiAndScopes map { definition =>
if (config.validateApiDefinition) {
apiDefinitionSchema.validate(new JSONObject(Json.toJson(definition).toString))
private def validateApiAndScopesAgainstSchema(apiAndScopes: ApiAndScopes): Either[PublishError, ApiAndScopes] = {
if (config.validateApiDefinition) {
Try(apiDefinitionSchema.validate(new JSONObject(Json.toJson(apiAndScopes).toString))) match {
case Success(_) => Right(apiAndScopes)
case Failure(ex: ValidationException) =>
logger.error(s"FAILED_TO_PUBLISH - Validation of API definition failed: ${ex.toJSON.toString(2)}", ex)
Left(DefinitionFileFailedSchemaValidation(Json.parse(ex.toJSON.toString)))
case Failure(ex) => Left(DefinitionFileFailedSchemaValidation(Json.parse(s"""{"Unexpected exception": "$ex.message"}""")))
}
definition
} else {
Right(apiAndScopes)
}
}

private def defaultCategories(apiAndScopes: Option[ApiAndScopes]) = {
apiAndScopes map { definition =>
if (definition.categories.isEmpty) {
val defaultCategories = categoryMap.getOrElse(definition.apiName, Seq(OTHER))
val updatedApi = definition.api ++ Json.obj("categories" -> defaultCategories)
definition.copy(api = updatedApi)
} else {
definition
}
private def defaultCategories(apiAndScopes: ApiAndScopes): ApiAndScopes = {
if (apiAndScopes.categories.isEmpty) {
val defaultCategories = categoryMap.getOrElse(apiAndScopes.apiName, Seq(OTHER))
val updatedApi = apiAndScopes.api ++ Json.obj("categories" -> defaultCategories)
apiAndScopes.copy(api = updatedApi)
} else {
apiAndScopes
}
}

Expand All @@ -122,47 +119,7 @@ class MicroserviceConnector @Inject() (
}

def getOAS(serviceLocation: ServiceLocation, version: String): Future[OpenAPI] = {
def handleSuccess(openApi: OpenAPI): Future[OpenAPI] = {
logger.info(s"Read OAS file from ${serviceLocation.serviceUrl}")
Future.successful(openApi)
}

def handleFailure(err: List[String]): Future[OpenAPI] = {
logger.warn(s"Failed to load OAS file from ${serviceLocation.serviceUrl} due to [${err.mkString}]")
Future.failed(new IllegalArgumentException("Cannot find valid OAS file"))
}

val parseOptions = new ParseOptions();
parseOptions.setResolve(true);
parseOptions.setResolveFully(true);
val emptyAuthList = java.util.Collections.emptyList[io.swagger.v3.parser.core.models.AuthorizationValue]()

val futureParsing = Future {
blocking {
try {
val parserResult = openAPIV3Parser.readLocation(oasFileLocator.locationOf(serviceLocation, version), emptyAuthList, parseOptions)

val outcome = (Option(parserResult.getMessages), Option(parserResult.getOpenAPI)) match {
case (Some(msgs), _) if msgs.size > 0 => Left(msgs.asScala.toList)
case (_, Some(openApi)) => Right(openApi)
case _ => Left(List("No errors or openAPI were returned from parsing"))
}

outcome
} catch {
case e: FileNotFoundException => Left(List(e.getMessage()))
}
}
}
.flatMap(outcome =>
outcome.fold(
err => handleFailure(err),
oasApi => handleSuccess(oasApi)
)
)

val futureTimer: Future[OpenAPI] = akka.pattern.after(config.oasParserMaxDuration, using = system.scheduler)(Future.failed(new IllegalStateException("Exceeded OAS parse time")))

Future.firstCompletedOf(List(futureParsing, futureTimer))
oasFileLoader.load(serviceLocation, version, config.oasParserMaxDuration)
}

}
96 changes: 96 additions & 0 deletions app/uk/gov/hmrc/apipublisher/connectors/OASFileLoader.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright 2023 HM Revenue & Customs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package uk.gov.hmrc.apipublisher.connectors

import java.io.FileNotFoundException
import javax.inject.{Inject, Singleton}
import scala.concurrent.duration.FiniteDuration
import scala.concurrent.{ExecutionContext, Future, blocking}
import scala.jdk.CollectionConverters._

import akka.actor.ActorSystem
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.parser.core.extensions.SwaggerParserExtension
import io.swagger.v3.parser.core.models.ParseOptions

import uk.gov.hmrc.apipublisher.models.ServiceLocation
import uk.gov.hmrc.apipublisher.util.ApplicationLogger

object OASFileLoader {

trait OASFileLocator {
def locationOf(serviceLocation: ServiceLocation, version: String): String
}

object MicroserviceOASFileLocator extends OASFileLocator {

def locationOf(serviceLocation: ServiceLocation, version: String): String =
s"${serviceLocation.serviceUrl}/api/conf/$version/application.yaml"
}

case class Config(validateApiDefinition: Boolean, oasParserMaxDuration: FiniteDuration)
}

@Singleton
class OASFileLoader @Inject() (oasFileLocator: OASFileLoader.OASFileLocator, openAPIV3Parser: SwaggerParserExtension)(implicit val ec: ExecutionContext, system: ActorSystem)
extends ApplicationLogger {

def load(serviceLocation: ServiceLocation, version: String, oasParserMaxDuration: FiniteDuration): Future[OpenAPI] = {
def handleSuccess(openApi: OpenAPI): Future[OpenAPI] = {
logger.info(s"Read OAS file from ${serviceLocation.serviceUrl}")
Future.successful(openApi)
}

def handleFailure(err: List[String]): Future[OpenAPI] = {
logger.warn(s"Failed to load OAS file from ${serviceLocation.serviceUrl} due to [${err.mkString}]")
Future.failed(new IllegalArgumentException("Cannot find valid OAS file"))
}

val parseOptions = new ParseOptions();
parseOptions.setResolve(true);
parseOptions.setResolveFully(true);
val emptyAuthList = java.util.Collections.emptyList[io.swagger.v3.parser.core.models.AuthorizationValue]()

val futureParsing = Future {
blocking {
try {
val parserResult = openAPIV3Parser.readLocation(oasFileLocator.locationOf(serviceLocation, version), emptyAuthList, parseOptions)

val outcome = (Option(parserResult.getMessages), Option(parserResult.getOpenAPI)) match {
case (Some(msgs), _) if msgs.size > 0 => Left(msgs.asScala.toList)
case (_, Some(openApi)) => Right(openApi)
case _ => Left(List("No errors or openAPI were returned from parsing"))
}

outcome
} catch {
case e: FileNotFoundException => Left(List(e.getMessage()))
}
}
}
.flatMap(outcome =>
outcome.fold(
err => handleFailure(err),
oasApi => handleSuccess(oasApi)
)
)

val futureTimer: Future[OpenAPI] = akka.pattern.after(oasParserMaxDuration, using = system.scheduler)(Future.failed(new IllegalStateException("Exceeded OAS parse time")))

Future.firstCompletedOf(List(futureParsing, futureTimer))
}
}
4 changes: 2 additions & 2 deletions app/uk/gov/hmrc/apipublisher/connectors/RamlLoader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ import javax.inject.{Inject, Singleton}

import uk.gov.hmrc.ramltools.loaders.{UrlRewriter, UrlRewritingRamlLoader}

import uk.gov.hmrc.apipublisher.wiring.AppContext
import uk.gov.hmrc.apipublisher.config.AppConfig

@Singleton
class DocumentationUrlRewriter @Inject() (appContext: AppContext) extends UrlRewriter {
class DocumentationUrlRewriter @Inject() (appContext: AppConfig) extends UrlRewriter {
lazy val rewrites = appContext.ramlLoaderRewrites
}

Expand Down
Loading

0 comments on commit 8202c9c

Please sign in to comment.