Skip to content

Commit

Permalink
Merge pull request #75 from hmrc/BDOG-238
Browse files Browse the repository at this point in the history
Bdog 238
  • Loading branch information
colin-lamed authored Jul 15, 2019
2 parents 88ef3e3 + a41905e commit 41f1291
Show file tree
Hide file tree
Showing 24 changed files with 1,240 additions and 89 deletions.
51 changes: 27 additions & 24 deletions app/uk/gov/hmrc/cataloguefrontend/AuthController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -37,31 +37,33 @@ 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 =>
authService
.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))
}
)
}
Expand All @@ -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
)
)

}
26 changes: 14 additions & 12 deletions app/uk/gov/hmrc/cataloguefrontend/actions/UmpAuthenticated.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,27 +29,26 @@ 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,
cc : MessagesControllerComponents
)(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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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))
Expand Down
27 changes: 8 additions & 19 deletions app/uk/gov/hmrc/cataloguefrontend/service/AuthService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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
)

}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
43 changes: 43 additions & 0 deletions app/uk/gov/hmrc/cataloguefrontend/shuttering/ShutterService.scala
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 41f1291

Please sign in to comment.