app/controllers/LoginComponents.scala (124 lines of code) (raw):
package controllers
import java.time.Duration
import com.github.t3hnar.bcrypt._
import com.gu.pandomainauth.action.AuthActions
import com.gu.pandomainauth.model.AuthenticatedUser
import com.gu.pandomainauth.{PanDomain, PanDomainAuthSettingsRefresher}
import com.gu.play.secretrotation.aws.parameterstore.{AwsSdkV1, SecretSupplier}
import com.gu.play.secretrotation.{RotatingSecretComponents, SnapshotProvider, TransitionTiming}
import config._
import play.api.ApplicationLoader.Context
import play.api.BuiltInComponentsFromContext
import play.api.libs.ws.WSClient
import play.api.libs.ws.ahc.AhcWSComponents
import play.api.mvc._
import play.filters.csrf.CSRFComponents
import play.filters.headers.SecurityHeadersComponents
import services.{EmergencyUser, EmergencyUserDBService, TokenDBService}
import utils.Loggable
import scala.concurrent.{ExecutionContext, Future}
abstract class LoginControllerComponents(
context: Context,
val aws: AWS
) extends BuiltInComponentsFromContext(context)
with AhcWSComponents
with AssetsComponents
with CSRFComponents
with SecurityHeadersComponents
with RotatingSecretComponents {
def httpFilters: Seq[EssentialFilter] = Seq(csrfFilter, securityHeadersFilter)
lazy val emergencyUserDBService = new EmergencyUserDBService(aws.dynamoDbClient, config.emergencyAccessTableName)
lazy val tokenDBService = new TokenDBService(aws.dynamoDbClient, config.tokensTableName)
def config: LoginConfig
def switches: Switches
lazy val asgTags: Option[InstanceTags] = aws.readTags()
val secretStateSupplier: SnapshotProvider = {
val stack = asgTags.map(_.stack).getOrElse("flexible")
val app = asgTags.map(_.app).getOrElse("login")
val stage = asgTags.map(_.stage).getOrElse("DEV")
new SecretSupplier(
TransitionTiming(usageDelay = Duration.ofMinutes(3), overlapDuration = Duration.ofHours(2)),
parameterName = s"/$stack/$app/$stage/play.http.secret.key",
AwsSdkV1(aws.ssmClient)
)
}
}
abstract class LoginController(
deps: LoginControllerComponents,
final override val panDomainSettings: PanDomainAuthSettingsRefresher
) extends BaseController with AuthActions with Loggable {
final override def wsClient: WSClient = deps.wsClient
final override def controllerComponents: ControllerComponents = deps.controllerComponents
final def config: LoginConfig = deps.config
final def switches: Switches = deps.switches
final override lazy val cacheValidation = true
override lazy val authCallbackUrl: String = config.host + "/oauthCallback"
final override def validateUser(authedUser: AuthenticatedUser): Boolean = PanDomain.guardianValidation(authedUser)
object EmergencySwitchIsOnAction extends ActionBuilder[Request, AnyContent] {
final override def parser: BodyParser[AnyContent] = deps.controllerComponents.parsers.default
final override def executionContext: ExecutionContext = deps.executionContext
override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]): Future[Result] = {
switches.allSwitches.get("emergency") match {
case Some(On) => block(request)
case Some(Off) => Future.successful(SeeOther("/emergency/reissue-disabled"))
case _ => Future.successful(BadRequest("Emergency reissue config switch is not configured correctly, value must be 'on' or 'off'."))
}
}
}
object EmergencySwitchChangeAccess extends ActionBuilder[Request, AnyContent] {
final override def parser: BodyParser[AnyContent] = deps.controllerComponents.parsers.default
final override def executionContext: ExecutionContext = deps.executionContext
override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]): Future[Result] = {
def checkPassword(user: EmergencyUser, username: String, password: String): Future[Result] = {
if (password.isBcryptedBounded(user.passwordHash)) {
log.info(s"$username is authorised to change the Emergency switch.")
block(request)
} else {
refuseSwitchChange(s"The password provided by $username is incorrect. User will be refused access to change emergency switch.")
}
}
def refuseSwitchChange(logErrorMsg: String): Future[Result] = {
log.warn(logErrorMsg)
Future.successful {
Unauthorized(
views.html.switches.switchChange(
"Authorisation checks failed, the Emergency switch will not be changed. Contact digitalcms.dev@theguardian.com for more help."
))
}
}
try {
val authHeaderUser = EmergencyActions.getBasicAuthDetails(request.headers)
val userId = authHeaderUser.id
val userOpt = deps.emergencyUserDBService.getUser(userId)
userOpt.map {
case Left(error) => refuseSwitchChange(s"Error with reading $userId from Dynamo: ${error.toString}. User will be refused access to change emergency switch.")
case Right(user) => checkPassword(user, userId, authHeaderUser.password)
}.getOrElse(refuseSwitchChange(s"User $userId not found. User will be refused access to change emergency switch."))
} catch {
case e: EmergencyActionsException =>
refuseSwitchChange(e.getMessage)
}
}
}
}
object EmergencyActions {
def getBasicAuthDetails(headers: Headers): AuthorizationHeaderUser = {
val authUserOpt = for {
authHeaders <- headers.toMap.get("Authorization")
basicAuthHead <- authHeaders.find(_.startsWith("Basic"))
} yield {
val basicAuthHeaderValue = basicAuthHead.split("Basic")(1).trim
if (!basicAuthHeaderValue.contains(":")) {
throw new EmergencyActionsException("Authorization header value is not the correct format.")
}
val usernameAndPassword = basicAuthHeaderValue.split(":")
if (usernameAndPassword.length != 2 || !usernameAndPassword(0).endsWith("@guardian.co.uk")) {
throw new EmergencyActionsException("Authorization header value is not the correct format.")
}
AuthorizationHeaderUser(usernameAndPassword(0).split("@guardian.co.uk")(0), usernameAndPassword(1))
}
authUserOpt.getOrElse(throw new EmergencyActionsException("Basic authorization header is missing"))
}
}
case class AuthorizationHeaderUser(id: String, password: String)
class EmergencyActionsException(message: String) extends Exception(message: String)