diff --git a/app/uk/gov/hmrc/cataloguefrontend/AuthController.scala b/app/uk/gov/hmrc/cataloguefrontend/AuthController.scala index dd8172b1a..41f327437 100644 --- a/app/uk/gov/hmrc/cataloguefrontend/AuthController.scala +++ b/app/uk/gov/hmrc/cataloguefrontend/AuthController.scala @@ -19,7 +19,7 @@ package uk.gov.hmrc.cataloguefrontend import javax.inject.{Inject, Singleton} import play.api.Configuration import play.api.data.Form -import play.api.data.Forms._ +import play.api.data.Forms import play.api.i18n.Messages import play.api.mvc._ import uk.gov.hmrc.cataloguefrontend.connector.UserManagementConnector.DisplayName @@ -37,17 +37,18 @@ class AuthController @Inject()( mcc: MessagesControllerComponents)(implicit ec: ExecutionContext) extends FrontendController(mcc) { - import AuthController.signinForm + import AuthController._ private[this] val selfServiceUrl = configuration.get[String]("self-service-url") - val showSignInPage: Action[AnyContent] = Action { implicit request => - Ok(sign_in(signinForm, selfServiceUrl)) - } + def showSignInPage(targetUrl: Option[String]): Action[AnyContent] = + Action { implicit request => + Ok(sign_in(signinForm.fill(SignInData(username = "", password = "", targetUrl = targetUrl)), selfServiceUrl)) + } val submit: Action[AnyContent] = Action.async { implicit request => - signinForm - .bindFromRequest() + val filledForm = signinForm.bindFromRequest + filledForm .fold( formWithErrors => Future.successful(BadRequest(sign_in(formWithErrors, selfServiceUrl))), signInData => @@ -55,13 +56,14 @@ class AuthController @Inject()( .authenticate(signInData.username, signInData.password) .map { case Right(TokenAndDisplayName(UmpToken(token), DisplayName(displayName))) => - Redirect(routes.CatalogueController.index()) + val targetUrl = signInData.targetUrl.getOrElse(routes.CatalogueController.index.url) + Redirect(targetUrl) .withSession( - UmpToken.SESSION_KEY_NAME -> token, - DisplayName.SESSION_KEY_NAME -> displayName - ) + UmpToken.SESSION_KEY_NAME -> token + , DisplayName.SESSION_KEY_NAME -> displayName + ) case Left(_) => - BadRequest(sign_in(signinForm.withGlobalError(Messages("sign-in.wrong-credentials")), selfServiceUrl)) + BadRequest(sign_in(filledForm.withGlobalError(Messages("sign-in.wrong-credentials")), selfServiceUrl)) } ) } @@ -76,20 +78,21 @@ class AuthController @Inject()( object AuthController { final case class SignInData( - username: String, - password: String - ) + username : String + , password : String + , targetUrl: Option[String] + ) private val signinForm = Form( - mapping( - "username" -> text, - "password" -> text - )(SignInData.apply)(SignInData.unapply) - .verifying( - "sign-in.wrong-credentials", - signInData => signInData.username.nonEmpty && signInData.password.nonEmpty - ) + Forms.mapping( + "username" -> Forms.text + , "password" -> Forms.text + , "targetUrl" -> Forms.optional(Forms.text) + )(SignInData.apply)(SignInData.unapply) + .verifying( + "sign-in.wrong-credentials", + signInData => signInData.username.nonEmpty && signInData.password.nonEmpty + ) ) - } diff --git a/app/uk/gov/hmrc/cataloguefrontend/actions/UmpAuthenticated.scala b/app/uk/gov/hmrc/cataloguefrontend/actions/UmpAuthenticated.scala index 075340095..38c6438fe 100644 --- a/app/uk/gov/hmrc/cataloguefrontend/actions/UmpAuthenticated.scala +++ b/app/uk/gov/hmrc/cataloguefrontend/actions/UmpAuthenticated.scala @@ -16,8 +16,11 @@ package uk.gov.hmrc.cataloguefrontend.actions +import cats.data.OptionT +import cats.implicits._ import javax.inject.{Inject, Singleton} import play.api.mvc._ +import uk.gov.hmrc.cataloguefrontend.{ routes => appRoutes } import uk.gov.hmrc.cataloguefrontend.connector.UserManagementAuthConnector import uk.gov.hmrc.cataloguefrontend.connector.UserManagementAuthConnector.UmpToken import uk.gov.hmrc.play.HeaderCarrierConverter @@ -26,6 +29,11 @@ import uk.gov.hmrc.http.HeaderCarrier import scala.concurrent.{ExecutionContext, Future} +/** Creates an Action will only proceed to invoke the action body, if there is a valid [[UmpToken]] in session. + * If there isn't, it will short circuit with a Redirect to SignIn page. + * + * Use [[VerifySignInStatus]] Action if you want to know if there is a valid token, but it should not terminate invocation. + */ @Singleton class UmpAuthenticated @Inject()( userManagementAuthConnector: UserManagementAuthConnector, @@ -33,20 +41,14 @@ class UmpAuthenticated @Inject()( )(implicit val ec: ExecutionContext) extends ActionBuilder[Request, AnyContent] { - def invokeBlock[A](request: Request[A], block: Request[A] => Future[Result]): Future[Result] = { implicit val hc: HeaderCarrier = HeaderCarrierConverter.fromHeadersAndSession(request.headers, Some(request.session)) - - request.session.get(UmpToken.SESSION_KEY_NAME) match { - case Some(token) => - userManagementAuthConnector.isValid(UmpToken(token)).flatMap { - case true => block(request) - case false => Future.successful(NotFound) - } - - case None => - Future.successful(NotFound) - } + OptionT( + request.session.get(UmpToken.SESSION_KEY_NAME) + .filterA(token => userManagementAuthConnector.isValid(UmpToken(token))) + ) + .semiflatMap(_ => block(request)) + .getOrElse(Redirect(appRoutes.AuthController.showSignInPage(targetUrl = Some(request.target.uriString).filter(_ => request.method == "GET")))) } override def parser: BodyParser[AnyContent] = cc.parsers.defaultBodyParser diff --git a/app/uk/gov/hmrc/cataloguefrontend/actions/VerifySignInStatus.scala b/app/uk/gov/hmrc/cataloguefrontend/actions/VerifySignInStatus.scala index 779ee72ed..4cf828fdb 100644 --- a/app/uk/gov/hmrc/cataloguefrontend/actions/VerifySignInStatus.scala +++ b/app/uk/gov/hmrc/cataloguefrontend/actions/VerifySignInStatus.scala @@ -29,6 +29,11 @@ import scala.concurrent.{ExecutionContext, Future} final case class UmpVerifiedRequest[A](request: Request[A], override val messagesApi: MessagesApi, isSignedIn: Boolean) extends MessagesRequest[A](request, messagesApi) +/** Creates an Action to check if there is a UmpToken, and if it is valid. + * It will continue to invoke the action body, with a [[UmpVerifiedRequest]] representing this status. + * + * Use [[UmpAuthenticated]] Action if it should only proceed when there is a valid UmpToken. + */ @Singleton class VerifySignInStatus @Inject()( userManagementAuthConnector: UserManagementAuthConnector, @@ -43,7 +48,7 @@ class VerifySignInStatus @Inject()( request.session.get(UmpToken.SESSION_KEY_NAME) match { case Some(token) => userManagementAuthConnector.isValid(UmpToken(token)).flatMap { isValid => - block(UmpVerifiedRequest(request, cc.messagesApi, isValid)) + block(UmpVerifiedRequest(request, cc.messagesApi, isSignedIn = isValid)) } case None => block(UmpVerifiedRequest(request, cc.messagesApi, isSignedIn = false)) diff --git a/app/uk/gov/hmrc/cataloguefrontend/service/AuthService.scala b/app/uk/gov/hmrc/cataloguefrontend/service/AuthService.scala index 83110c623..de307aa93 100644 --- a/app/uk/gov/hmrc/cataloguefrontend/service/AuthService.scala +++ b/app/uk/gov/hmrc/cataloguefrontend/service/AuthService.scala @@ -16,7 +16,7 @@ package uk.gov.hmrc.cataloguefrontend.service -import cats.data.EitherT +import cats.data.{EitherT, OptionT} import cats.implicits._ import javax.inject.{Inject, Singleton} import uk.gov.hmrc.cataloguefrontend.connector.UserManagementAuthConnector.{UmpToken, UmpUnauthorized, UmpUserId} @@ -34,30 +34,19 @@ class AuthService @Inject()( )(implicit val ec: ExecutionContext) { def authenticate(username: String, password: String)( - implicit hc: HeaderCarrier): Future[Either[UmpUnauthorized, TokenAndDisplayName]] = { - - def getDisplayNameOrDefaultToUserId(userId: UmpUserId): Future[Either[UmpUnauthorized, DisplayName]] = - userManagementConnector.getDisplayName(userId).map { - case Some(displayName) => Right(displayName) - case None => Right(DisplayName(userId.value)) - } - + implicit hc: HeaderCarrier): Future[Either[UmpUnauthorized, TokenAndDisplayName]] = (for { - umpAuthData <- EitherT(userManagementAuthConnector.authenticate(username, password)) - displayName <- EitherT(getDisplayNameOrDefaultToUserId(umpAuthData.userId)) - } yield { - TokenAndDisplayName(umpAuthData.token, displayName) - }).value - - } - + umpAuthData <- EitherT(userManagementAuthConnector.authenticate(username, password)) + optDisplayName <- EitherT.liftF[Future, UmpUnauthorized, Option[DisplayName]](userManagementConnector.getDisplayName(umpAuthData.userId)) + displayName = optDisplayName.getOrElse(DisplayName(umpAuthData.userId.value)) + } yield TokenAndDisplayName(umpAuthData.token, displayName) + ).value } object AuthService { final case class TokenAndDisplayName( - token: UmpToken, + token : UmpToken, displayName: DisplayName ) - } diff --git a/app/uk/gov/hmrc/cataloguefrontend/shuttering/ShutterConnector.scala b/app/uk/gov/hmrc/cataloguefrontend/shuttering/ShutterConnector.scala new file mode 100644 index 000000000..14643fbf9 --- /dev/null +++ b/app/uk/gov/hmrc/cataloguefrontend/shuttering/ShutterConnector.scala @@ -0,0 +1,91 @@ +/* + * Copyright 2019 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.cataloguefrontend.shuttering + +import javax.inject.{Inject, Singleton} +import play.api.Logger +import play.api.libs.json.Reads +import uk.gov.hmrc.http.HeaderCarrier +import uk.gov.hmrc.play.bootstrap.config.ServicesConfig +import uk.gov.hmrc.play.bootstrap.http.HttpClient + +import scala.concurrent.{ExecutionContext, Future} + +@Singleton +class ShutterConnector @Inject()( + http : HttpClient + , serviceConfig: ServicesConfig + )(implicit val ec: ExecutionContext){ + + private val urlStates: String = s"${serviceConfig.baseUrl("shutter-api")}/shutter-api/states" + private val urlEvents: String = s"${serviceConfig.baseUrl("shutter-api")}/shutter-api/events" + + private implicit val ssr = ShutterState.reads + private implicit val ser = ShutterEvent.reads + + /** + * GET + * /shutter-api/states + * Retrieves the current shutter states for all applications in all environments + */ + def shutterStates()(implicit hc: HeaderCarrier): Future[Seq[ShutterState]] = + http.GET[Seq[ShutterState]](url = urlStates) + + /** + * GET + * /shutter-api/events + * Retrieves the current shutter events for all applications for given environment + */ + def latestShutterEvents(env: Environment)(implicit hc: HeaderCarrier): Future[Seq[ShutterStateChangeEvent]] = + http.GET[Seq[ShutterEvent]](url = s"$urlEvents?type=${EventType.ShutterStateChange.asString}&namedFilter=latestByServiceName&data.environment=${env.asString}") + .map(_.flatMap(_.toShutterStateChangeEvent)) + + /** + * GET + * /shutter-api/states/{appName} + * Retrieves the current shutter states for the given application in all environments + */ + def shutterStateByApp(appName: String)(implicit hc: HeaderCarrier): Future[Option[ShutterState]] = + http.GET[Option[ShutterState]](s"$urlStates/$appName") + + + /** + * GET + * /shutter-api/states/{appName}/{environment} + * Retrieves the current shutter state for the given application in the given environment + */ + def shutterStatusByAppAndEnv(appName: String, env: Environment)(implicit hc: HeaderCarrier): Future[Option[ShutterStatus]] = { + implicit val ssf = ShutterStatus.format + http.GET[Option[ShutterStatus]](s"$urlStates/$appName/${env.asString}") + } + + + /** + * PUT + * /shutter-api/states/{appName}/{environment} + * Shutters/un-shutters the application in the given environment + */ + def updateShutterStatus(appName: String, env: Environment, status: ShutterStatus)(implicit hc: HeaderCarrier): Future[Unit] = { + implicit val isf = ShutterStatus.format + + implicit val ur = new uk.gov.hmrc.http.HttpReads[Unit] { + def read(method: String, url: String, response: uk.gov.hmrc.http.HttpResponse): Unit = () + } + + http.PUT[ShutterStatus, Unit](s"$urlStates/$appName/${env.asString}", status) + } +} diff --git a/app/uk/gov/hmrc/cataloguefrontend/shuttering/ShutterController.scala b/app/uk/gov/hmrc/cataloguefrontend/shuttering/ShutterController.scala new file mode 100644 index 000000000..15ae47023 --- /dev/null +++ b/app/uk/gov/hmrc/cataloguefrontend/shuttering/ShutterController.scala @@ -0,0 +1,51 @@ +/* + * Copyright 2019 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.cataloguefrontend.shuttering + +import javax.inject.{Inject, Singleton} +import play.api.Logger +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import uk.gov.hmrc.cataloguefrontend.actions.VerifySignInStatus +import uk.gov.hmrc.play.bootstrap.controller.FrontendController +import views.html.shuttering._ + +import scala.concurrent.{ExecutionContext, Future} +import scala.util.control.NonFatal + +@Singleton +class ShutterController @Inject()( + mcc : MessagesControllerComponents + , verifySignInStatus: VerifySignInStatus + , shutterStatePage : ShutterStatePage + , shutterService : ShutterService + )(implicit val ec: ExecutionContext) + extends FrontendController(mcc) { + + def allStates(envParam: String): Action[AnyContent] = + verifySignInStatus.async { implicit request => + val env = Environment.parse(envParam).getOrElse(Environment.Production) + for { + currentState <- shutterService.findCurrentState(env) + .recover { + case NonFatal(ex) => + Logger.error(s"Could not retrieve currentState: ${ex.getMessage}", ex) + Seq.empty + } + page = shutterStatePage(currentState, env, request.isSignedIn) + } yield Ok(page) + } +} diff --git a/app/uk/gov/hmrc/cataloguefrontend/shuttering/ShutterService.scala b/app/uk/gov/hmrc/cataloguefrontend/shuttering/ShutterService.scala new file mode 100644 index 000000000..f4f0dfa47 --- /dev/null +++ b/app/uk/gov/hmrc/cataloguefrontend/shuttering/ShutterService.scala @@ -0,0 +1,43 @@ +/* + * Copyright 2019 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.cataloguefrontend.shuttering + +import java.time.LocalDateTime + +import javax.inject.{Inject, Singleton} +import uk.gov.hmrc.http.HeaderCarrier + +import scala.concurrent.{ExecutionContext, Future} + +@Singleton +class ShutterService @Inject()(shutterConnector: ShutterConnector)(implicit val ec: ExecutionContext) { + + def getShutterStates(implicit hc: HeaderCarrier): Future[Seq[ShutterState]] = + shutterConnector.shutterStates + + def updateShutterStatus(serviceName: String, env: Environment, status: ShutterStatus)(implicit hc: HeaderCarrier): Future[Unit] = + shutterConnector.updateShutterStatus(serviceName, env, status) + + def findCurrentState(env: Environment)(implicit hc: HeaderCarrier): Future[Seq[ShutterStateChangeEvent]] = + for { + events <- shutterConnector.latestShutterEvents(env) + sorted = events.sortWith { + case (l, r) => l.status == ShutterStatus.Shuttered || + l.serviceName < r.serviceName + } + } yield sorted +} diff --git a/app/uk/gov/hmrc/cataloguefrontend/shuttering/ShutterServiceController.scala b/app/uk/gov/hmrc/cataloguefrontend/shuttering/ShutterServiceController.scala new file mode 100644 index 000000000..3bd9cd157 --- /dev/null +++ b/app/uk/gov/hmrc/cataloguefrontend/shuttering/ShutterServiceController.scala @@ -0,0 +1,199 @@ +/* + * Copyright 2019 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.cataloguefrontend.shuttering + +import cats.data.EitherT +import cats.instances.all._ +import cats.syntax.all._ +import javax.inject.{Inject, Singleton} +import play.api.data.{Form, Forms} +import play.api.i18n.MessagesProvider +import play.api.libs.json.{Format, Json} +import play.api.mvc.{Action, MessagesControllerComponents, Request, Result, Session} +import play.twirl.api.Html +import uk.gov.hmrc.play.bootstrap.controller.FrontendController +import uk.gov.hmrc.cataloguefrontend.actions.UmpAuthenticated +import uk.gov.hmrc.cataloguefrontend.connector.SlugInfoFlag +import uk.gov.hmrc.cataloguefrontend.shuttering.{ routes => appRoutes } +import views.html.shuttering.shutterService.{Page1, Page2, Page3} + +import scala.concurrent.{ExecutionContext, Future} + +@Singleton +class ShutterServiceController @Inject()( + mcc : MessagesControllerComponents + , shutterService : ShutterService + , page1 : Page1 + , page2 : Page2 + , page3 : Page3 + , umpAuthenticated: UmpAuthenticated + )(implicit val ec : ExecutionContext + ) extends FrontendController(mcc) + with play.api.i18n.I18nSupport { + + import ShutterServiceController._ + + private def start(form: Form[ShutterForm])(implicit request: Request[Any]): Future[Html] = + for { + shutterStates <- shutterService.getShutterStates + envs = Environment.values + statusValues = ShutterStatus.values + } yield page1(form, shutterStates, envs, statusValues) + + def step1Get(env: Option[String], serviceName: Option[String]) = + umpAuthenticated.async { implicit request => + for { + shutterStates <- shutterService.getShutterStates + shutter = if (serviceName.isDefined || env.isDefined) { + ShutterForm( + serviceNames = serviceName.toSeq + , env = env.getOrElse("") + , status = statusFor(shutterStates)(serviceName, env).fold("")(_.asString) + ) + } else fromSession(request.session) match { + case Some(shutter) => ShutterForm( + serviceNames = shutter.serviceNames + , env = shutter.env.asString + , status = shutter.status.asString + ) + case None => ShutterForm(serviceNames = Seq.empty, env = "", status = "") + } + html <- start(form.fill(shutter)).map(Ok(_)) + } yield html + } + + def statusFor(shutterStates: Seq[ShutterState])(optServiceName: Option[String], optEnv: Option[String]): Option[ShutterStatus] = + for { + serviceName <- optServiceName + envStr <- optEnv + env <- Environment.parse(envStr) + status <- shutterStates.find(_.name == serviceName).map(_.statusFor(env)) + } yield status match { + case ShutterStatus.Shuttered => ShutterStatus.Unshuttered + case ShutterStatus.Unshuttered => ShutterStatus.Shuttered + } + + def step1Post = + umpAuthenticated.async { implicit request => + (for { + sf <- form + .bindFromRequest + .fold( + hasErrors = formWithErrors => EitherT.left(start(formWithErrors).map(BadRequest(_))) + , success = data => EitherT.pure[Future, Result](data) + ) + env <- Environment.parse(sf.env) match { + case Some(env) => EitherT.pure[Future, Result](env) + case None => EitherT.left(start(form.bindFromRequest).map(BadRequest(_))) + } + status <- ShutterStatus.parse(sf.status) match { + case Some(status) => EitherT.pure[Future, Result](status) + case None => EitherT.left(start(form.bindFromRequest).map(BadRequest(_))) + } + shutter = Shutter(sf.serviceNames, env, status) + } yield Redirect(appRoutes.ShutterServiceController.step2Get) + .withSession(request.session + toSession(shutter)) + ).merge + } + + def step2Get = + umpAuthenticated { implicit request => + fromSession(request.session) + .map(sf => Ok(page2(sf))) + .getOrElse(Redirect(appRoutes.ShutterServiceController.step1Post)) + } + + def step2Post = + umpAuthenticated.async { implicit request => + (for { + shutter <- EitherT.fromOption[Future]( + fromSession(request.session) + , Redirect(appRoutes.ShutterServiceController.step1Post) + ) + _ <- shutter.serviceNames.toList.traverse_[EitherT[Future, Result, ?], Unit] { serviceName => + EitherT.right[Result] { + shutterService + .updateShutterStatus(serviceName, shutter.env, shutter.status) + } + } + } yield Redirect(appRoutes.ShutterServiceController.step3Get) + ).merge + } + + def step3Get = + umpAuthenticated { implicit request => + fromSession(request.session) + .map(shutter => Ok(page3(shutter)).withSession(request.session - SessionKey)) + .getOrElse(Redirect(appRoutes.ShutterServiceController.step1Post)) + } + + + def form(implicit messagesProvider: MessagesProvider) = + Form( + Forms.mapping( + "serviceName" -> Forms.seq(Forms.text).verifying(notEmptySeq) + , "env" -> Forms.text.verifying(notEmpty) + , "status" -> Forms.text.verifying(notEmpty) + )(ShutterForm.apply)(ShutterForm.unapply) + ) + + // Forms.nonEmpty, but has no constraint info label + def notEmpty = { + import play.api.data.validation._ + Constraint[String]("") { o => + if (o == null || o.trim.isEmpty) Invalid(ValidationError("error.required")) else Valid + } + } + + def notEmptySeq = { + import play.api.data.validation._ + Constraint[Seq[String]]("") { o => + if (o == null || o.isEmpty) Invalid(ValidationError("error.required")) else Valid + } + } +} + +object ShutterServiceController { + case class ShutterForm( + serviceNames: Seq[String] + , env : String + , status : String + ) + + case class Shutter( + serviceNames: Seq[String] + , env : Environment + , status : ShutterStatus + ) + + val SessionKey = "ShutterServiceController" + + private implicit val shutterFormats = { + implicit val ef = Environment.format + implicit val ssf = ShutterStatus.format + Json.format[Shutter] + } + + def toSession(sf: Shutter): (String, String) = + (SessionKey -> Json.stringify(Json.toJson(sf)(shutterFormats))) + + def fromSession(session: Session): Option[Shutter] = + for { + js <- session.get(SessionKey) + sf <- Json.parse(js).asOpt[Shutter] + } yield sf +} diff --git a/app/uk/gov/hmrc/cataloguefrontend/shuttering/ShutterStateModel.scala b/app/uk/gov/hmrc/cataloguefrontend/shuttering/ShutterStateModel.scala new file mode 100644 index 000000000..5838271ba --- /dev/null +++ b/app/uk/gov/hmrc/cataloguefrontend/shuttering/ShutterStateModel.scala @@ -0,0 +1,286 @@ +/* + * Copyright 2019 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.cataloguefrontend.shuttering + +import java.time.Instant +import play.api.libs.json.{Format, Json, JsError, JsObject, JsValue, JsPath, JsResult, JsString, JsSuccess, Reads, Writes, __} +import play.api.libs.functional.syntax._ + + +sealed trait Environment { def asString: String } +object Environment { + case object Production extends Environment { val asString = "production" } + case object ExternalTest extends Environment { val asString = "externalTest" } + case object QA extends Environment { val asString = "qa" } + case object Staging extends Environment { val asString = "staging" } + case object Dev extends Environment { val asString = "development" } + + val values = List(Production, ExternalTest, QA, Staging, Dev) + + def parse(s: String): Option[Environment] = + values.find(_.asString == s) + + val format: Format[Environment] = new Format[Environment] { + override def reads(json: JsValue) = + json.validate[String] + .flatMap { s => + parse(s) match { + case Some(env) => JsSuccess(env) + case None => JsError(__, s"Invalid Environment '$s'") + } + } + + override def writes(e: Environment) = + JsString(e.asString) + } +} + +sealed trait ShutterStatus { def asString: String } +object ShutterStatus { + case object Shuttered extends ShutterStatus { val asString = "shuttered" } + case object Unshuttered extends ShutterStatus { val asString = "unshuttered" } + + val values = List(Shuttered, Unshuttered) + + def parse(s: String): Option[ShutterStatus] = + values.find(_.asString == s) + + + val format: Format[ShutterStatus] = new Format[ShutterStatus] { + override def reads(json: JsValue) = + json.validate[String] + .flatMap { s => + parse(s) match { + case Some(env) => JsSuccess(env) + case None => JsError(__, s"Invalid ShutterStatus '$s'") + } + } + + override def writes(e: ShutterStatus) = + JsString(e.asString) + } +} + +case class ShutterState( + name : String + , production : ShutterStatus + , staging : ShutterStatus + , qa : ShutterStatus + , externalTest: ShutterStatus + , development : ShutterStatus + ) { + def statusFor(env: Environment): ShutterStatus = + env match { + case Environment.Production => production + case Environment.ExternalTest => externalTest + case Environment.QA => qa + case Environment.Staging => staging + case Environment.Dev => development + } + } + +object ShutterState { + + val reads: Reads[ShutterState] = { + implicit val ssf = ShutterStatus.format + ( (__ \ "name" ).read[String] + ~ (__ \ "production" ).read[ShutterStatus] + ~ (__ \ "staging" ).read[ShutterStatus] + ~ (__ \ "qa" ).read[ShutterStatus] + ~ (__ \ "externalTest").read[ShutterStatus] + ~ (__ \ "development" ).read[ShutterStatus] + )(ShutterState.apply _) + } +} + + + +// -------------- Events --------------------- + + +sealed trait EventType { def asString: String } +object EventType { + case object ShutterStateCreate extends EventType { override val asString = "shutter-state-create" } + case object ShutterStateDelete extends EventType { override val asString = "shutter-state-delete" } + case object ShutterStateChange extends EventType { override val asString = "shutter-state-change" } + case object KillSwitchStateChange extends EventType { override val asString = "killswitch-state-change" } + + + val values = List(ShutterStateCreate, ShutterStateDelete, ShutterStateChange, KillSwitchStateChange) + + def parse(s: String): Option[EventType] = + values.find(_.asString == s) + + val format: Format[EventType] = new Format[EventType] { + override def reads(json: JsValue) = + json.validate[String] + .flatMap { s => + parse(s) match { + case Some(et) => JsSuccess(et) + case None => JsError(__, s"Invalid EventType '$s'") + } + } + + override def writes(e: EventType) = + JsString(e.asString) + } +} + +sealed trait ShutterCause { def asString: String } +object ShutterCause { + case object Scheduled extends ShutterCause { override val asString = "scheduled" } + case object UserCreated extends ShutterCause { override val asString = "user-shutter"} + + val values = List(Scheduled, UserCreated) + + def parse(s: String): Option[ShutterCause] = + values.find(_.asString == s) + + val format: Format[ShutterCause] = new Format[ShutterCause] { + override def reads(json: JsValue) = + json.validate[String] + .flatMap { s => + parse(s) match { + case Some(et) => JsSuccess(et) + case None => JsError(__, s"Invalid ShutterCause '$s'") + } + } + + override def writes(e: ShutterCause) = + JsString(e.asString) + } + +} + +sealed trait EventData +object EventData { + case class ShutterStateCreateData( + serviceName: String + ) extends EventData + + case class ShutterStateDeleteData( + serviceName: String + ) extends EventData + + case class ShutterStateChangeData( + serviceName: String + , environment: Environment + , status : ShutterStatus + , cause : ShutterCause + ) extends EventData + + case class KillSwitchStateChangeData( + environment: Environment + , status : ShutterStatus + ) extends EventData + + + val shutterStateCreateDataFormat: Format[ShutterStateCreateData] = + (__ \ "serviceName").format[String] + .inmap( ShutterStateCreateData.apply + , unlift(ShutterStateCreateData.unapply) + ) + + val shutterStateDeleteDataFormat: Format[ShutterStateDeleteData] = + (__ \ "serviceName").format[String] + .inmap( ShutterStateDeleteData.apply + , unlift(ShutterStateDeleteData.unapply) + ) + + val shutterStateChangeDataFormat: Format[ShutterStateChangeData] = { + implicit val ef = Environment.format + implicit val ssf = ShutterStatus.format + implicit val scf = ShutterCause.format + + ( (__ \ "serviceName").format[String] + ~ (__ \ "environment").format[Environment] + ~ (__ \ "status" ).format[ShutterStatus] + ~ (__ \ "cause" ).format[ShutterCause] + )( ShutterStateChangeData.apply + , unlift(ShutterStateChangeData.unapply) + ) + } + + val killSwitchStateChangeDataFormat: Format[KillSwitchStateChangeData] = { + implicit val ef = Environment.format + implicit val ssf = ShutterStatus.format + + ( (__ \ "environment").format[Environment] + ~ (__ \ "status" ).format[ShutterStatus] + )( KillSwitchStateChangeData.apply + , unlift(KillSwitchStateChangeData.unapply + )) + } + + def reads(et: EventType) = new Reads[EventData] { + implicit val sscrdf = shutterStateCreateDataFormat + implicit val ssddf = shutterStateDeleteDataFormat + implicit val sscdf = shutterStateChangeDataFormat + implicit val kscdf = killSwitchStateChangeDataFormat + def reads(js: JsValue): JsResult[EventData] = + et match { + case EventType.ShutterStateCreate => js.validate[EventData.ShutterStateCreateData] + case EventType.ShutterStateDelete => js.validate[EventData.ShutterStateDeleteData] + case EventType.ShutterStateChange => js.validate[EventData.ShutterStateChangeData] + case EventType.KillSwitchStateChange => js.validate[EventData.KillSwitchStateChangeData] + } + } +} + +case class ShutterEvent( + username : String + , timestamp: Instant + , eventType: EventType + , data : EventData + ) { + def toShutterStateChangeEvent: Option[ShutterStateChangeEvent] = + data match { + case sscd: EventData.ShutterStateChangeData => + Some(ShutterStateChangeEvent( + username = username + , timestamp = timestamp + , serviceName = sscd.serviceName + , environment = sscd.environment + , status = sscd.status + , cause = sscd.cause + )) + case _ => None + } + } + +/** Special case flattened */ +case class ShutterStateChangeEvent( + username : String + , timestamp : Instant + , serviceName: String + , environment: Environment + , status : ShutterStatus + , cause : ShutterCause + ) + +object ShutterEvent { + + val reads: Reads[ShutterEvent] = { + implicit val etf = EventType.format + ( (__ \ "username" ).read[String] + ~ (__ \ "timestamp").read[Instant] + ~ (__ \ "type" ).read[EventType] + ~ (__ \ "type" ).read[EventType] + .flatMap[EventData](et => (__ \ "data").read(EventData.reads(et))) + )(ShutterEvent.apply _) + } +} \ No newline at end of file diff --git a/app/views/SearchByUrlPage.scala.html b/app/views/SearchByUrlPage.scala.html index b69768e2c..9777d93d4 100644 --- a/app/views/SearchByUrlPage.scala.html +++ b/app/views/SearchByUrlPage.scala.html @@ -44,7 +44,7 @@