app/controllers/Application.scala (180 lines of code) (raw):
package controllers
import java.io.FileInputStream
import java.util.Properties
import javax.inject.{Inject, Singleton}
import play.api._
import play.api.mvc._
import auth.{BearerTokenAuth, LDAP, Security, User}
import com.unboundid.ldap.sdk.LDAPConnectionPool
import helpers.DatabaseHelper
import models.{LoginRequest, LoginRequestSerializer}
import play.api.cache.SyncCacheApi
import play.api.http.HttpEntity
import play.api.libs.json._
import scala.collection.JavaConverters._
import scala.concurrent.Future
import scala.util.{Failure, Success, Try}
import scala.concurrent.ExecutionContext.Implicits.global
@Singleton
class Application @Inject() (val cc:ControllerComponents,
override val bearerTokenAuth:BearerTokenAuth,
p:PlayBodyParsers,
override implicit val config:Configuration,
cacheImpl:SyncCacheApi,
dbHelper:DatabaseHelper)
extends AbstractController(cc) with Security with LoginRequestSerializer {
implicit val cache:SyncCacheApi = cacheImpl
/**
* Action to provide base html and frontend code to the client
* @param path http path postfix, not used but must be included to allow an indefinite path in routes
* @return Action containing html
*/
def index(path:String) = Action {
val cbVersionString = try {
val prop = new Properties()
prop.load(getClass.getClassLoader.getResourceAsStream("version.properties"))
Option(prop.getProperty("build-sha"))
} catch {
case e:Throwable=>
logger.warn("Could not get build-sha property: ", e)
None
}
Ok(views.html.index(cbVersionString.getOrElse("none"), config.getOptional[String]("deployment-root").getOrElse(""), config.getOptional[String]("vaultdoor_server_url").getOrElse("")))
}
def timeoutTest(delay: Int) = Action {
Thread.sleep(delay*1000)
Ok(Json.obj("status"->"ok","delay"->(delay*1000)))
}
/**
* Action to allow the client to authenticate. Expects a JSON body containing username and password (use https!!!)
* @return If login is successful, a 200 response containing a session cookie that authenticates the user.
* If unsuccessful, a 403 response
* If the data is malformed, a 400 response
* If an error occurs, a 500 response with a basic error message directing the user to go to the logs
*/
def authenticate = Action(p.json) { request=>
val adminRoles = config.getOptional[Seq[String]]("ldap.admin-groups").getOrElse(List("Administrator"))
logger.info(s"Admin roles are: $adminRoles")
LDAP.connectionPool.fold(
errors=> {
logger.error("LDAP not configured properly", errors)
InternalServerError(Json.obj("status" -> "error", "detail" -> "ldap not configured properly, see logs"))
},
ldapConnectionPool=> {
implicit val pool: LDAPConnectionPool = ldapConnectionPool
request.body.validate[LoginRequest].fold(
errors => {
BadRequest(Json.obj("status" -> "error", "detail" -> JsError.toJson(errors)))
},
loginRequest => {
User.authenticate(loginRequest.username, loginRequest.password) match {
case Success(Some(user)) =>
Ok(Json.obj("status" -> "ok", "detail" -> "Logged in", "uid" -> user.uid, "isAdmin"->checkRole(user.uid, adminRoles)))
.withSession("uid" -> user.uid)
case Success(None) =>
logger.warn(s"Failed login from ${loginRequest.username} with password ${loginRequest.password} from host ${request.host}")
Forbidden(Json.obj("status" -> "error", "detail" -> "forbidden"))
case Failure(error) =>
logger.error(s"Authentication error when trying to log in ${loginRequest.username}. This could just mean a wrong password.", error)
Forbidden(Json.obj("status" -> "error", "detail" -> "forbidden"))
}
})
}
)
}
def checkCorsOrigins(request:Request[AnyContent]) = {
logger.debug(s"checkCorsOrigins: current origin is ${request.headers.get("Origin")}")
if(!request.headers.hasHeader("Origin")) Left("CORS not applicable")
config.getOptional[Seq[String]]("external.allowedFrontendDomains") match {
case Some(allowedDomainsList)=>
logger.debug(s"checkCorsOrigins: allowed urls are $allowedDomainsList")
if(allowedDomainsList.contains(request.headers("Origin"))) Right(request.headers("Origin")) else Left(s"${request.headers("Origin")} is not an allowed domain")
case None=>Left("No allowed origins configured")
}
}
/**
* respond to CORS options requests for login from vaultdoor
* see https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
* @return
*/
def corsOptions = Action { request=>
checkCorsOrigins(request) match {
case Right(allowedOrigin) =>
val returnHeaders = Map(
"Access-Control-Allow-Methods" -> "POST, PUT, OPTIONS",
"Access-Control-Allow-Origin" -> allowedOrigin,
"Access-Control-Allow-Headers" -> "content-type, authorization",
)
Result(
ResponseHeader(204, returnHeaders),
HttpEntity.NoEntity
)
case Left(other) =>
logger.warn(s"Invalid CORS preflight request for authentication: $other")
Forbidden("")
}
}
/**
* Action that allows the frontend to test if the current session is valid
* @return If the session is not valid, a 403 response
* If the session is valid, a 200 response with the currently logged in userid in a json object
*/
def isLoggedIn = IsAuthenticated { uid=> { request=>
val isAdmin = checkAdmin(uid, request)
val corsOrigin = if(request.headers.hasHeader("Origin")){
checkCorsOrigins(request)
} else {
Left("")
}
val result = Ok(Json.obj("status"->"ok","uid"->uid, "isAdmin"->isAdmin))
corsOrigin.map(org=>"Access-Control-Allow-Origin"->org) match {
case Right(updated)=>
logger.debug(s"Adding headers: $updated")
result.withHeaders(updated, "Access-Control-Allow-Credentials"->"true")
case Left(err)=>
logger.debug(s"Could not get cors data: $err")
result.withHeaders("something"->"somethingelse")
}
}}
/**
* Action that allows the frontend to test if the user is an admin
* @return If the user is not an admin, a 403 response. If the user is an admin, a 200 response
*/
def checkIsAdmin = IsAdmin {uid=> {request=>
Ok(Json.obj("status"->"ok"))
}}
/**
* Action to log out, by clearing the client's session cookie.
* @return
*/
def logout = Action { request=>
Ok(Json.obj("status"->"ok","detail"->"Logged out")).withNewSession
}
/**
* test raise an exception
*/
def testexception = Action { request=>
throw new RuntimeException("This is a test exception")
}
/**
* test raise an exception that is caught and logged
*/
def testcaughtexception = Action { request=>
try{
throw new RuntimeException("This is a test exception that was caught")
} catch {
case e:Throwable=>
logger.error("Testcaughtexception", e)
Ok(Json.obj("status"->"ok","detail"->"test exception was caught"))
}
}
def getPublicDsn = Action { request=>
try {
val prop = new Properties()
prop.load(getClass.getClassLoader.getResourceAsStream("sentry.properties"))
val dsnString = prop.getProperty("public-dsn")
if(dsnString==null)
NotFound(Json.obj("status"->"error","detail"->"property public-dsn was not set"))
else
Ok(Json.obj("status"->"ok","publicDsn"->dsnString))
} catch {
case e:Throwable=>
logger.error("Could not get publicDsn property: ", e)
InternalServerError(Json.obj("status"->"error","detail"->e.toString))
}
}
def makeHealthcheckBody(statusString:String, ldapCheck: Try[Unit], dbCheck:Try[Unit]) = {
val ldapString = ldapCheck match {
case Success(x)=>"ok"
case Failure(err)=>err.toString
}
val dbString = dbCheck match {
case Success(x)=>"ok"
case Failure(err)=>err.toString
}
Json.obj(
"status"->statusString,
"ldap"->ldapString,
"database"->dbString
)
}
def healthcheck = Action.async { request=>
val checkFutures = Future.sequence(Seq(dbHelper.healthcheck))
checkFutures.map(resultSeq=>{
val ldapCheck = Success( () ) //dont b0rk on a failing ldap check
val dbCheck = resultSeq.head
val ldapMode = config.getOptional[String]("ldap.ldapProtocol").getOrElse("ldap")
if( (ldapMode!="none" && ldapCheck.isFailure) || dbCheck.isFailure){
if(ldapCheck.isFailure) logger.error(s"LDAP Healthcheck is failing: ${ldapCheck.failed.get.toString}")
if(dbCheck.isFailure) logger.error(s"DB Healthcheck is failing: ${dbCheck.failed.get.toString}")
InternalServerError(makeHealthcheckBody("error",ldapCheck,dbCheck))
} else{
Ok(makeHealthcheckBody("ok",ldapCheck,dbCheck))
}
})
}
}