Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ tmp
.settings
.target
.idea*
*.iml
*.log
jruby/vendor
*.csv
Expand All @@ -23,3 +24,5 @@ RUNNING_PID
.worksheet
*.sc
.ensime*
build.sbt
out/
10 changes: 1 addition & 9 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,7 @@ General
<tbody>
<tr>
<td class="resource">GET /</td>
<td class="description">Returns the default template, currently index.scala.html. The user must first be authenticated via OpenID.</td>
</tr>
<tr>
<td class="resource">GET /login</td>
<td class="description">If a user is not authenticated, handles redirecting to Google Apps login. Google Apps login will subsequently redirect back to /loginCallback for verification. The user is redirected to /index if there is a valid session.</td>
</tr>
<tr>
<td class="resource">GET /loginCallback</td>
<td class="description">A callback URI used by OpenID once the authentication sequence has completed on the server.</td>
<td class="description">Returns the default template, currently index.scala.html. The user must first be authenticated via mozilla persona.</td>
</tr>
<tr>
<td class="resource">GET /user</td>
Expand Down
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,6 @@ Try hitting the server with your browser:

<http://localhost:9000>

You should see a Rearview page with a message indicating you must login via Google (you must use a
GMail account which matches the configured domain for openid).

To deploy on Amazon EC2 with an RDS instance, follow the detailed instructions [here](https://github.com/livingsocial/rearview/wiki/Setting-up-Rearview-on-EC2-with-Amazon-RDS).


Expand Down
17 changes: 0 additions & 17 deletions app/rearview/Global.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import play.api.Mode
import play.api.Play
import play.api.Play.current
import play.api.db.DB
import play.api.libs.openid.OpenIDError
import play.api.mvc.Action
import play.api.mvc.Flash
import play.api.mvc.Handler
Expand Down Expand Up @@ -89,22 +88,6 @@ object Global extends WithFilters(LoggingFilter) {
}


/**
* Through trial and error I determined the onError method must be overridden to handle OpenID auth errors. This
* may not be the best place for this, but until I find better documentation this works.
*/
override def onError(request: RequestHeader, ex: Throwable) = {
ex.getCause() match {
case e: OpenIDError =>
Logger.warn("Unauthorized OpenID attempt")
implicit val flash = Flash(Map("auth-error" -> "We were unable to authenticate you, please try again."))
Forbidden(views.html.unauthorized())

case _ => super.onError(request, ex)
}
}


/**
* Load active jobs from the database and add the to the scheduler. Subsequent jobs are added/removed via the
* JobController/Scheduler interfaces.
Expand Down
97 changes: 83 additions & 14 deletions app/rearview/controller/MainController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,44 @@ import rearview.Global
import rearview.dao.UserDAO
import rearview.model.ModelImplicits.userFormat
import rearview.model.User

object MainController extends Controller with Security with OpenIDLogin {
import play.api.Routes
import play.api.Logger
import play.api.mvc.{Controller, Action}
import scala.concurrent.Future
import play.api.libs.json.{Json, JsValue}
import play.api.libs.ws.WS
import scala.util.{Success, Failure}
import scala.concurrent.Await
import scala.concurrent.duration._
import play.api.mvc.Security.{username => defaultUsername}

object MainController extends Controller with Security {
val USER = "user"

def callbackUri()(implicit request: Request[Any]) = routes.MainController.loginCallback().absoluteURL()
def successUri()(implicit request: Request[Any]) = routes.MainController.index().absoluteURL()

implicit val context = scala.concurrent.ExecutionContext.Implicits.global

def index = Authenticated { implicit request =>
Ok(views.html.index(Global.uiVersion))
}


def user = Authenticated { implicit request =>
Ok(request.session(USER))
}


def adduser = Authenticated { implicit request =>
Ok(views.html.adduser())
}

def addNewUser(email: String, firstName: String, lastName: String) = Authenticated { implicit request =>
Logger.info("adding new user...")
Some(UserDAO.store(User(None, email, firstName, lastName, Some(new Date))))
Redirect(routes.MainController.index)
}

def unauthorized = Action { implicit request =>
Forbidden(views.html.unauthorized())
Forbidden(views.html.unauthorized()).withNewSession
}


Expand All @@ -41,14 +59,65 @@ object MainController extends Controller with Security with OpenIDLogin {
Ok(views.html.test())
}


protected def userFromOpenID(email: String, firstName: String, lastName: String) = {
val user = UserDAO.findByEmail(email) map { user =>
UserDAO.store(user.copy(lastLogin = Some(new Date)))
} orElse {
Some(UserDAO.store(User(None, email, firstName, lastName, Some(new Date))))
def request(host: String, assertion: String):Future[JsValue] = {
val verifier = "https://verifier.login.persona.org/verify"
Logger.info("requesting persona verification...")
Logger.info("host=" + host)
Logger.info("assertion=" + assertion)
WS.url(verifier)
.withHeaders("Content-Type" -> "application/x-www-form-urlencoded")
.post(
Map(
"assertion" -> Seq(assertion),
"audience" -> Seq("https://" + host)
)
).map( response => Json.parse(response.body))
}

def personaVerify(host: String, assertion: String):Future[(Boolean, String)] = {
val result = request(host, assertion)
Logger.info("mapping result...")
result.map(json => {
(json \ "status").asOpt[String].map(_ == "okay") match {
case Some(true) =>
(json \ "email").asOpt[String] match {
case Some(email) => (true, email)
case None => (false, "Persona verifier not working correctly")
}
case Some(false) =>
(false, (json \ "reason").asOpt[String].getOrElse("Unknown reason"))
}
}).recover {
case e: Exception =>
Logger.info("Could not parse the response from persona verifier")
e.printStackTrace()
(false, e.getMessage)
}

user.map(toJson(_))
}

def verify(assertion: String) = Action { implicit request =>
Logger.info("Verifying Persona request:")
Logger.info("request=" + request)
Logger.info("request.host=" + request.host)
Logger.info("request.domain=" + request.domain)
val f = personaVerify(request.host, assertion).map({
case (true, email: String) =>

val user = UserDAO.findByEmail(email) map { user =>
UserDAO.store(user.copy(lastLogin = Some(new Date)))
}
Logger.info("user = "+user)
if(user != None){
user.map(toJson(_))
Redirect(successUri).withSession(defaultUsername -> email, MainController.USER -> toJson(user).toString())
} else{
Redirect(routes.MainController.unauthorized + "/logout")
}

case (false, reason: String) =>
Logger.info("Forbidden: " + reason)
Forbidden(reason)
})
Await.result(f, 10 seconds)
}
}
44 changes: 0 additions & 44 deletions app/rearview/controller/Security.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import play.api.Play.configuration
import play.api.Play.current
import play.api.libs.json.JsValue
import play.api.libs.json.Json.toJson
import play.api.libs.openid.OpenID
import play.api.mvc.Action
import play.api.mvc.AnyContent
import play.api.mvc.AsyncResult
Expand Down Expand Up @@ -42,48 +41,5 @@ trait Security {
def Authenticated(f: AuthenticatedRequest[AnyContent] => Result): Action[AnyContent] = {
Authenticated(parse.anyContent)(f)
}
}

trait OpenIDLogin {
lazy val authDomain = configuration.getString("openid.domain").getOrElse(sys.error("openid.domain must be defined in configuration"))
lazy val openidHost = configuration.getString("openid.host").getOrElse(sys.error("openid.host must be defined in configuration"))

def callbackUri()(implicit request: Request[Any]): String
def successUri()(implicit request: Request[Any]): String

def login = Action { implicit request =>
val attrs = "email" -> "http://axschema.org/contact/email" ::
"firstName" -> "http://axschema.org/namePerson/first" ::
"lastName" -> "http://axschema.org/namePerson/last" :: Nil
AsyncResult(OpenID.redirectURL(openidHost, callbackUri, attrs) map { url =>
Redirect(url)
} recover {
case e: Throwable =>
Redirect(routes.MainController.unauthorized)
})
}

def loginCallback = Action { implicit request =>
Logger.debug("loginCallback: " + request)
AsyncResult(OpenID.verifiedId map { userInfo =>
userInfo.attributes.get("email") match {
case Some(email) if(email.endsWith("@" + authDomain)) =>
userFromOpenID(email, userInfo.attributes("firstName"), userInfo.attributes("lastName")) match {
case Some(user) => Redirect(successUri).withSession(defaultUsername -> email, MainController.USER -> toJson(user).toString())
case _ => Redirect(successUri).withSession(defaultUsername -> email)
}

case other =>
Logger.info("Redeemed, but no email: " + other)
Redirect(routes.MainController.unauthorized)
}
} recover {
case e: Throwable =>
Logger.debug("Failed login", e)
Redirect(routes.MainController.unauthorized)
})
}


protected def userFromOpenID(email: String, firstName: String, lastName: String): Option[JsValue]
}
2 changes: 1 addition & 1 deletion app/rearview/model/Model.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ case class Application(id: Option[Long],
modifiedAt: Option[Date] = None)

/**
* User object which is dynamically created from a successful openid auth sequence.
* User object which is dynamically created from a successful mozilla persona auth sequence.
* @param id
* @param email
* @param firstName
Expand Down
27 changes: 27 additions & 0 deletions app/views/adduser.scala.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<title>Add User</title>
<meta charset='utf-8' />

<link rel="stylesheet" href="../../public/css/persona-buttons.css" />
<script src="https://login.persona.org/include.js"></script>

</head>

<body bgcolor="#333333">
<div align="center">
<font size="5" face="Calibri" color="#ffffaa">Please enter the user information<br>for the new user.</font><br><br>
<font size="4" face="Calibri" color="#ffffff">Email:</font><br>
<input type="text" id="email"><br><br>
<font size="4" face="Calibri" color="#ffffff" style="margin-right:50px">First Name:</font>
<font size="4" face="Calibri" color="#ffffff" style="margin-left:50px">Last Name:</font><br>
<input type="text" id="first">
<input type="text" id="last"><br><br>
<button class="persona-button orange" onclick="cancelAdding()"><span>Cancel</span></button>
<button class="persona-button" onclick="addUser()"><span>Add user</span></button>
<br><br><font size="3" face="Calibri" color="#ff3333" id="hint"></font>
<script src='../../public/js/persona.js'></script>
</div>
</body>
</html>
14 changes: 12 additions & 2 deletions app/views/index.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@

<link href="@routes.Assets.at("/public", "less/rearview.less")" rel='stylesheet/less' type='text/css' />
<script src="@routes.Assets.at("/public", "vendor/less/js/less.js")" type='text/javascript'></script>


<link rel="stylesheet" href="../../public/css/persona-buttons.css" />
<script src="https://login.persona.org/include.js"></script>
<!-- <link href="@routes.Assets.at("/public", "css/rearview.css")" rel='stylesheet' /> -->

<!--[if lt IE 9]>
Expand All @@ -33,7 +35,14 @@
</head>

<body class='ls-rearview'>
<section class='alert-wrap clearfix'></section>

<div align="right" style="margin-top:50px">
<button onclick='showAddUser()' class='persona-button dark'><span>Add new user</span></button>
<button onclick='signout()' class='persona-button orange'><span>Sign out</span></button>
<script src='../../public/js/persona.js'></script>
</div>

<section class='alert-wrap clearfix'></section>
<section class='primary-nav-wrap clearfix'></section>
<section class='header-wrap clearfix'></section>
<section class='timeline-wrap clearfix'></section>
Expand All @@ -51,6 +60,7 @@
rearview.version = "@version";
</script>
<script data-main="@routes.Assets.at("/public", "js/main")" src="@routes.Assets.at("/public", "vendor/require/js/require.js")"></script>

</body>
</html>
}
10 changes: 8 additions & 2 deletions app/views/unauthorized.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@

<link href="@routes.Assets.at("/public", "less/login.less")" rel='stylesheet/less' type='text/css' />
<script src="@routes.Assets.at("/public", "vendor/less/js/less.js")" type='text/javascript'></script>


<link rel="stylesheet" href="../../public/css/persona-buttons.css" />
<script src="https://login.persona.org/include.js"></script>

</head>
<body class='login rearview'>
<!--[if lt IE 8]><p class=chromeframe>Your browser is <em>ancient!</em> <a href='http://browsehappy.com/'>Upgrade to a different browser</a> or <a href='http://www.google.com/chromeframe/?redirect=true'>install Google Chrome Frame</a> to experience this site.</p><![endif]-->
Expand All @@ -31,7 +34,10 @@ <h2>Real-Time Monitoring</h2>
</hgroup>
</header>
<div class='authentication-box'>
<a href='login' class='btn btn-primary'>Sign in with Google apps</a>
<h1></h1>
<button onclick='signin()' class='persona-button'><span>Sign in</span></button><br>
<h2 id="hint"></h2>
<script src='../../public/js/persona.js'></script>
</div>
</section>
</section>
Expand Down
7 changes: 3 additions & 4 deletions conf/common.conf
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ graphite.timeout=10000
# Alerts
# ~~~~~~
# An array of class names to load on startup for alerting
alert.class_names = []
alert.class_names = ["rearview.alert.LiveEmailAlert"]

# Enable alerts (Email, PagerDuty ..) for Graphite and other internal errors (disabled by default).
# This means that alerts will only be sent when the monitor successfully runs and raises an alert.
Expand All @@ -133,16 +133,15 @@ statsd.port=8125
# Hostname used for generating links back to service
service.hostname="localhost:9000"

openid.host="https://www.google.com/accounts/o8/id"
openid.domain="hungrymachine.com"

ruby.script="ruby/monitor.rb"
ruby.sandbox.timeout=5

# Email
email.from="rearview@livingsocial.com"
email.host="localhost"
email.port=25
email.user=""
email.password=""

# Tread lightly with these settings
internal-threadpool-size=4
Expand Down
7 changes: 5 additions & 2 deletions conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
# This file defines all application routes (Higher priority routes first)
# ~~~~

GET /verify/:assertion rearview.controller.MainController.verify(assertion)
GET /adduser rearview.controller.MainController.adduser
GET /adduser/:email/:first/:last rearview.controller.MainController.addNewUser(email, first, last)

# Auth hooks
GET /login rearview.controller.MainController.login
GET /loginCallback rearview.controller.MainController.loginCallback
GET /unauthorized rearview.controller.MainController.unauthorized
GET /unauthorized/logout rearview.controller.MainController.unauthorized

# General routes
GET / rearview.controller.MainController.index
Expand Down
Loading