Permalink
Browse files

Refactoring of REST and HTTP Auth

Combined the two and cleaned up the design a bit.
  • Loading branch information...
1 parent ef1d346 commit d8091ff285465fe3ec279e00902e97bab5ef2e44 @dchenbecker dchenbecker committed Oct 10, 2010
@@ -1,19 +1,24 @@
package bootstrap.liftweb
+import java.sql.{Connection, DriverManager}
+
import net.liftweb.common.{Box,Empty,Failure,Full,Logger}
-import net.liftweb.util.Helpers
-import net.liftweb.http.{LiftRules,ParsePath,Req,RewriteRequest,RewriteResponse}
+import net.liftweb.util.{Helpers,LoanWrapper}
+import net.liftweb.http.{GetRequest,LiftRules,ParsePath,PutRequest,Req,
+ RequestVar,RewriteRequest,RewriteResponse,S}
import net.liftweb.sitemap.{Loc,Menu,SiteMap}
-import net.liftweb.mapper.{DB, ConnectionManager, Schemifier, DefaultConnectionIdentifier, ConnectionIdentifier}
-import java.sql.{Connection, DriverManager}
-import com.pocketchangeapp.model._
-import com.pocketchangeapp.api._
-import com.pocketchangeapp.util.{Charting,Image}
+import net.liftweb.mapper.{By,DB,ConnectionManager,Schemifier,
+ DefaultConnectionIdentifier,ConnectionIdentifier}
// Get implicit conversions
import net.liftweb.sitemap.Loc._
import net.liftweb.util.Helpers._
+import com.pocketchangeapp.model._
+import com.pocketchangeapp.api._
+import com.pocketchangeapp.util.{Charting,Image}
+
+
/**
* The bootstrap.liftweb.Boot class is the main entry point for Lift.
@@ -23,6 +28,9 @@ import net.liftweb.util.Helpers._
* TODO: Connect Lucene/Compass for search
*/
class Boot {
+ // Set up a logger to use for startup messages
+ val logger = Logger(classOf[Boot])
+
def boot {
/*
* LiftRules.early allows us to apply functions to the request before
@@ -43,16 +51,21 @@ class Boot {
if (!DB.jndiJdbcConnAvailable_?) DB.defineConnectionManager(DefaultConnectionIdentifier, DBVendor)
-
+ // This method is here due to type conflicts when attempting to use
+ // a bare method.
def schemeLogger (msg : => AnyRef) = {
- Logger(classOf[Boot]).info(msg)
+ logger.info(msg)
}
- Schemifier.schemify(true, schemeLogger _, User, Tag, Account, AccountAdmin, AccountViewer, AccountNote, Expense, ExpenseTag)
+ Schemifier.schemify(true, schemeLogger _,
+ User, Tag, Account, AccountAdmin,
+ AccountViewer, AccountNote, Expense, ExpenseTag)
LiftRules.setSiteMap(SiteMap(MenuInfo.menu :_*))
+ // Tie in the REST API. Uncomment the one you want to use
LiftRules.dispatch.prepend(DispatchRestAPI.dispatch)
+ // LiftRules.dispatch.prepend(HelperRestAPI)
// Set up some rewrites
LiftRules.statelessRewrite.append {
@@ -74,10 +87,33 @@ class Boot {
() => Full(Image.viewImage(expenseId))
}
- import scala.xml.Text
- val m = Title(if (User.loggedIn_?) { Text("a") } else { Text("b") })
+ // Hook in our REST API auth
+ LiftRules.httpAuthProtectedResource.append(DispatchRestAPI.protection)
+
+ /* We're going to use HTTP Basic auth for REST, although
+ * technically this allows for its use anywhere in the app. */
+ import net.liftweb.http.auth.{AuthRole,HttpBasicAuthentication,userRoles}
+ LiftRules.authentication = HttpBasicAuthentication("PocketChange") {
+ case (userEmail, userPass, _) => {
+ logger.debug("Authenticating: " + userEmail)
+ User.find(By(User.email, userEmail)).map { user =>
+ if (user.password.match_?(userPass)) {
+ logger.debug("Auth succeeded for " + userEmail)
+ User.logUserIn(user)
+
+ // Compute all of the user roles
+ userRoles(user.editable.map(acct => AuthRole("editAcct:" + acct.id)) ++
+ user.allAccounts.map(acct => AuthRole("viewAcct:" + acct.id)))
+ true
+ } else {
+ logger.warn("Auth failed for " + userEmail)
+ false
+ }
+ } openOr false
+ }
+ }
- Logger(classOf[Boot]).info("Bootstrap up")
+ logger.info("Bootstrap up")
}
}
@@ -1,5 +1,8 @@
/*
* RestAPI.scala
+ *
+ * Copyright 2008-2010 Derek Chen-Becker, Marius Danciu and Tyler Wier
+ *
*/
package com.pocketchangeapp {
package api {
@@ -8,189 +11,101 @@ import java.text.SimpleDateFormat
import scala.xml.{Elem, Node, NodeSeq, Text}
-import net.liftweb.common.{Box,Empty,Full,Logger}
-import net.liftweb.http.{AtomResponse,BadResponse,CreatedResponse,GetRequest,JsonResponse,LiftResponse,LiftRules,NotFoundResponse,ParsePath,PutRequest,Req,RewriteRequest}
+import net.liftweb.common.{Box,Empty,Failure,Full,Logger}
+import net.liftweb.http.{AtomResponse,BadResponse,CreatedResponse,
+ GetRequest,JsonResponse,LiftResponse,LiftRules,
+ NotFoundResponse,ParsePath,PutRequest,Req,
+ ResponseWithReason,RewriteRequest}
import net.liftweb.http.rest.XMLApiHelper
-import net.liftweb.http.js.JsExp
-import net.liftweb.http.js.JE.JsRaw
-import net.liftweb.mapper.{By,MaxRows}
+import net.liftweb.mapper.By
import model._
-/**
- * This object provides some conversion and formatting specific to our
- * REST API.
- */
-object RestFormatters {
- /* The REST timestamp format. Not threadsafe, so we create
- * a new one each time. */
- def timestamp = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
-
- // A simple helper to generate the REST ID of an Expense
- def restId (e : Expense) =
- "http://www.pocketchangeapp.com/api/expense/" + e.id
+object DispatchRestAPI extends XMLApiHelper {
+ final val logger = Logger("com.pocketchangeapp.api.DispatchRestAPI")
- // A simple helper to generate the REST timestamp of an Expense
- def restTimestamp (e : Expense) : String =
- timestamp.format(e.dateOf.is)
+ // Import our methods for converting things around
+ import RestFormatters._
/**
- * Generates the XML REST representation of an Expense
- */
- def toXML (e : Expense) : Elem =
- <expense>
- <id>{restId(e)}</id>
- <accountname>{e.accountName}</accountname>
- <date>{restTimestamp(e)}</date>
- <description>{e.description.is}</description>
- <amount>{e.amount.is.toString}</amount>
- <tags>
- {e.tags.flatMap {t => <tag>{t.name.is}</tag>}}
- </tags>
- </expense>
-
- /*
- * Generates the JSON REST representation of an Expense
- */
- def toJSON (e : Expense) : JsExp = {
- import net.liftweb.json.JsonDSL._
- import net.liftweb.json.JsonAST._
-
- val entry =
- ("id" -> restId(e)) ~
- ("date" -> restTimestamp(e)) ~
- ("description" -> e.description.is) ~
- ("accountname" -> e.accountName) ~
- ("amount" -> e.amount.is.toString) ~
- ("tags" -> e.tags.map(_.name.is))
-
- JsRaw(compact(render(entry)))
- }
-
- /*
- * Generates an Atom 1.0 feed from the last 10 Expenses for the given
- * account.
+ * This method provides the dispatch hooks for our REST API that
+ * we'll hook into LiftRules.dispatch
*/
- def toAtom (a : Account) : Elem = {
- val entries = Expense.getByAcct(a,Empty,Empty,Empty,MaxRows(10))
-
- <feed xmlns="http://www.w3.org/2005/Atom">
- <title>{a.name}</title>
- <id>urn:uuid:{a.uuid.is}</id>
- <updated>{a.entries.headOption.map(restTimestamp) getOrElse
- timestamp.format(new java.util.Date)}</updated>
- { a.entries.flatMap(toAtom) }
- </feed>
- }
-
- /*
- * Generates the XML Atom representation of an Expense
- */
- def toAtom (e : Expense) : Elem =
- <entry>
- <id>urn:uuid:{restId(e)}</id>
- <title>{e.description.is}</title>
- <updated>{restTimestamp(e)}</updated>
- <content type="xhtml">
- <div xmlns="http://www.w3.org/1999/xhtml">
- <table>
- <tr><th>Amount</th><th>Tags</th><th>Receipt</th></tr>
- <tr><td>{e.amount.is.toString}</td>
- <td>{e.tags.map(_.name.is).mkString(", ")}</td>
- <td>{
- if (e.receipt.is ne null) {
- <img src={"/image/" + e.id} />
- } else Text("None")
- }</td></tr>
- </table>
- </div>
- </content>
- </entry>
-}
-
-object DispatchRestAPI extends XMLApiHelper {
- // Import our implicits for converting things around
- import RestFormatters._
-
def dispatch: LiftRules.DispatchPF = {
// Define our getters first
- case Req(List("api", "expense", Expense(e)), _, GetRequest) =>
- () => Full(nodeSeqToResponse(toXML(e))) // default to XML
- case Req(List("api", "expense", Expense(e), "xml"), _, GetRequest) =>
- () => Full(nodeSeqToResponse(toXML(e))) // explicitly XML
- case Req(List("api", "expense", Expense(e), "json"), _, GetRequest) =>
- () => JsonResponse(toJSON(e), Nil, Nil, 200) // explicitly JSON
- case Req(List("api", "account", Account(a)), _, GetRequest) =>
- () => AtomResponse(toAtom(a)) // explicit atom request
+ case Req(List("api", "expense", Expense(expense,_)), _, GetRequest) =>
+ () => nodeSeqToResponse(toXML(expense)) // default to XML
+ case Req(List("api", "expense", Expense(expense,_), "xml"), _, GetRequest) =>
+ () => nodeSeqToResponse(toXML(expense))
+ case Req(List("api", "expense", Expense(expense,_), "json"), _, GetRequest) =>
+ () => JsonResponse(toJSONExp(expense), Nil, Nil, 200)
+ case Req(List("api", "account", Account(account)), _, GetRequest) =>
+ () => AtomResponse(toAtom(account))
// Define the PUT handler
- case r @ Req("api" :: "expense" :: Nil, _, PutRequest) =>
- () => addExpense(r)
+ case request @ Req(List("api", "account", Account(account)), _, PutRequest) =>
+ () => addExpense(account,request)
// Invalid API request - route to our error handler
case Req("api" :: x :: Nil, "", _) =>
() => BadResponse() // Everything else fails
}
+ /*
+ * This partial function defines how we protect our API
+ * using the HttpAuthentication functionality in Lift. This
+ * will hook into LiftRules.httpAuthProtectedResource.
+ */
+ import net.liftweb.http.auth.AuthRole
+ def protection : LiftRules.HttpAuthProtectedResourcePF = {
+ case Req(List("api", "account", accountId), _, PutRequest) =>
+ Full(AuthRole("editAcct:" + accountId))
+ case Req(List("api", "account", accountId), _, GetRequest) =>
+ Full(AuthRole("viewAcct:" + accountId))
+ // If the account is public, don't enforce auth
+ case Req(List("api", "expense", Expense(e, true)), _, GetRequest) => Empty
+ case Req(List("api", "expense", Expense(e, _)), _, GetRequest) =>
+ Full(AuthRole("viewAcct:" + e.account.obj.open_!.id))
+ }
+
def createTag (xml : NodeSeq) : Elem = <pca_api>{xml}</pca_api>
// reacts to the PUT Request
- def addExpense(req: Req): LiftResponse = {
- var tempEmail = ""
- var tempPass = ""
- var tempAccountUUID = ""
-
- var expense = new Expense
- req.xml match {
- case Full(<expense>{parameters @ _*}</expense>) => {
- for(parameter <- parameters){ parameter match {
- case <email>{email}</email> => tempEmail = email.text
- case <password>{password}</password> => tempPass = password.text
- case <accountUUID>{uuid}</accountUUID> => tempAccountUUID = uuid.text
- case <dateOf>{dateof}</dateOf> => expense.dateOf(new java.util.Date(dateof.text))
- case <amount>{value}</amount> => expense.amount(BigDecimal(value.text))
- case <desc>{description}</desc> => expense.description(description.text)
- case _ =>
- }
- }
-
- try {
- val u:User = User.find(By(User.email, tempEmail)) match {
- case Full(user) if user.validated && user.password.match_?(tempPass) =>
- User.logUserIn(user)
- user
- case _ => new User
- }
-
- val currentAccount = Account.find(By(Account.owner, u.id.is), By(Account.uuid, tempAccountUUID)).open_!
- expense.account(currentAccount.id.is)
-
- val (entrySerial,entryBalance) = Expense.getLastExpenseData(currentAccount, expense.dateOf)
-
- expense.account(currentAccount).serialNumber(entrySerial + 1).tags("api").currentBalance(entryBalance + expense.amount)
-
+ def addExpense(account : Account, req: Req): LiftResponse = {
+ RestFormatters.fromXML(req.xml, account) match {
+ case Full(expense) => {
+ val (entrySerial,entryBalance) =
+ Expense.getLastExpenseData(account, expense.dateOf)
+
+ expense.account(account).serialNumber(entrySerial + 1).
+ currentBalance(entryBalance + expense.amount)
+
expense.validate match {
- case Nil =>
+ case Nil => {
Expense.updateEntries(entrySerial + 1, expense.amount.is)
expense.save
- println(currentAccount.name)
- //val acct = Account.find(currentAccount.name.is).open_!
- val newBalance = currentAccount.balance.is + expense.amount.is
- currentAccount.balance(newBalance).save
-
- CreatedResponse(expense.toXml, "text/xml")
- case _ =>
- BadResponse() // TODO: Return a meaningful error
+
+ account.balance(account.balance.is + expense.amount.is).save
+
+ CreatedResponse(toXML(expense), "text/xml")
+ }
+ case errors => {
+ val message = errors.mkString("Validation failed:", ",","")
+ logger.error(message)
+ ResponseWithReason(BadResponse(), message)
+ }
}
}
- catch {
- case e => Logger(this.getClass).error("Could not add expense", e); BadResponse()
+ case Failure(msg, _, _) => {
+ logger.error(msg)
+ ResponseWithReason(BadResponse(), msg)
+ }
+ case error => {
+ logger.error("Parsed expense as : " + error)
+ BadResponse()
}
- }
- case _ => Logger(this.getClass).error("Request was malformed"); BadResponse()
}
}
-
}
Oops, something went wrong.

0 comments on commit d8091ff

Please sign in to comment.