refactored plan.scala and its ReadWriteWeb class in order to seperate more cleanly the different parts of the plan. This makes it easier to wrap the right part of the plan with an access control wrapper. It also makes it a little bit easier to understand. Pursuing this line of thinking could also allow more flexibility later. Also fixed a nullpointer exception, which should go in a different patch, but I am not good enough at mercurial.
--- a/src/main/scala/Filesystem.scala Sun Oct 16 22:47:37 2011 +0200
+++ b/src/main/scala/Filesystem.scala Mon Oct 17 12:52:37 2011 +0200
@@ -51,6 +51,7 @@
case RDFRepr(rdfLang) => rdfLang
case _ => lang
}
+ case _ => lang
}
if (fileOnDisk.exists()) {
val fis = new FileInputStream(fileOnDisk)
--- a/src/main/scala/ReadWriteWebMain.scala Sun Oct 16 22:47:37 2011 +0200
+++ b/src/main/scala/ReadWriteWebMain.scala Mon Oct 17 12:52:37 2011 +0200
@@ -1,6 +1,6 @@
package org.w3.readwriteweb
-import auth.{RDFAuthZ, AuthZ, X509view}
+import auth.{RDFAuthZ, X509view}
import org.w3.readwriteweb.util._
import unfiltered.jetty._
--- a/src/main/scala/auth/Authz.scala Sun Oct 16 22:47:37 2011 +0200
+++ b/src/main/scala/auth/Authz.scala Mon Oct 17 12:52:37 2011 +0200
@@ -25,14 +25,14 @@
import unfiltered.filter.Plan
import unfiltered.request._
-import unfiltered.response.Unauthorized
import collection.JavaConversions._
import javax.security.auth.Subject
-import javax.servlet.http.HttpServletRequest
import java.net.URL
import org.w3.readwriteweb.{Resource, ResourceManager, WebCache}
import com.hp.hpl.jena.query.{QueryExecutionFactory, QueryExecution, QuerySolutionMap, QueryFactory}
import sun.management.resources.agent
+import unfiltered.response.{ResponseFunction, Unauthorized}
+import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
/**
@@ -73,16 +73,18 @@
}
object NullAuthZ extends AuthZ {
- def subject(req: NullAuthZ.HSRequest) = null
+ def subject(req: NullAuthZ.Req) = null
def guard(m: Method, path: String) = null
- override def protect(in: Plan.Intent) = in
+ override def protect(in: Req=>Res) = in
}
abstract class AuthZ {
- type HSRequest = HttpRequest[HttpServletRequest]
+ type Req = HttpRequest[HttpServletRequest]
+ type Res = ResponseFunction[HttpServletResponse]
+
// I need a guard
// - in order to be able to have different implementations, but subclassing could do to
// - the guard should get the information from the file system or the authdb, so it should know where those are
@@ -90,17 +92,16 @@
// I will need a web cache to get the subject
- def protect(in: Plan.Intent): Plan.Intent = {
- req: HSRequest => req match {
- case HttpMethod(method) & Path(path) if guard(method, path).allow(() => subject(req)) => in(req)
+ def protect(in: Req=>Res): Req=>Res = {
+ case req @ HttpMethod(method) & Path(path) if guard(method, path).allow(() => subject(req)) => in(req)
case _ => Unauthorized
}
- }
+
- def subject(req: HSRequest): Option[Subject]
+ protected def subject(req: Req): Option[Subject]
/** create the guard defined in subclass */
- def guard(m: Method, path: String): Guard
+ protected def guard(m: Method, path: String): Guard
abstract class Guard(m: Method, path: String) {
@@ -118,7 +119,7 @@
import AuthZ.x509toSubject
implicit val cache : WebCache = webCache
- def subject(req: HSRequest) = req match {
+ def subject(req: Req) = req match {
case X509Claim(claim) => Option(claim)
case _ => None
}
--- a/src/main/scala/plan.scala Sun Oct 16 22:47:37 2011 +0200
+++ b/src/main/scala/plan.scala Mon Oct 17 12:52:37 2011 +0200
@@ -3,9 +3,6 @@
import auth.{AuthZ, NullAuthZ}
import org.w3.readwriteweb.util._
-import unfiltered.request._
-import unfiltered.response._
-
import scala.io.Source
import java.net.URL
@@ -20,6 +17,10 @@
QueryTypeDescribe => DESCRIBE}
import scalaz._
+import unfiltered.filter.Plan
+import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
+import unfiltered.request._
+import unfiltered.response._
//object ReadWriteWeb {
//
@@ -30,17 +31,40 @@
//}
class ReadWriteWeb(rm: ResourceManager, implicit val authz: AuthZ = NullAuthZ) {
-
+ // a few type short cuts to make it easier to reason with the code here
+ // one may want to generalise this code so that it does not depend so strongly on servlets.
+ type Req = HttpRequest[HttpServletRequest]
+ type Res = ResponseFunction[HttpServletResponse]
+
val logger: Logger = LoggerFactory.getLogger(this.getClass)
- /** I believe some documentation is needed here, as many different tricks
- * are used to make this code easy to read and still type-safe
- *
- * Planify.apply takes an Intent, which is defined in Cycle by
- * type Intent [-A, -B] = PartialFunction[HttpRequest[A], ResponseFunction[B]]
- * the corresponding syntax is: case ... => ...
- *
- * this code makes use of ScalaZ Validation. For example of how to use it, see
+
+ /**
+ * The Servlet Filter for the ReadWriteWeb
+ *
+ * Planify.apply takes an Intent, which is defined in Cycle by
+ * type Intent [-A, -B] = PartialFunction[HttpRequest[A], ResponseFunction[B]]
+ * the corresponding syntax is: case ... => ...
+ */
+ //bblfish: I don't understand why this has to be lazy, but if it is not, it the plan ends up throwing a null
+ //pointer exception.
+ lazy val plan = unfiltered.filter.Planify(intent)
+
+ /**
+ * The partial function that if triggered sends to the readwrite web code.
+ * It wraps the ReadWriteWeb function with the AuthZ passed in the argument
+ * ( Note that we don't want to protect this intent, since that would be to apply the security to all other applications,
+ * many of which may want different authorization implementations )
+ */
+ val intent : Plan.Intent = {
+ case req @ Path(path) if path startsWith rm.basePath => authz.protect(readWriteWebIntent)(req)
+ }
+
+ /**
+ * The core ReadWrite web function
+ * ( This is not a partial function and so is not a Plan.Intent )
+ *
+ * This code makes use of ScalaZ Validation. For example of how to use it, see
* http://scalaz.googlecode.com/svn/continuous/latest/browse.sxr/scalaz/example/ExampleValidation.scala.html
*
* the Resource abstraction returns Validation[Throwable, ?something]
@@ -55,99 +79,100 @@
* At last, Validation[ResponseFunction, ResponseFuntion] is exposed as a ResponseFunction
* through another implicit conversion. It saves us the call to the Validation.fold() method
*/
- val plan = unfiltered.filter.Planify {
- authz.protect {
- case req @ Path(path) if path startsWith rm.basePath => {
- val Authoritative(uri, representation) = req
- val r: Resource = rm.resource(uri)
- req match {
- case GET(_) if representation == HTMLRepr => {
- val source = Source.fromFile("src/main/resources/skin.html")("UTF-8")
- val body = source.getLines.mkString("\n")
- Ok ~> ViaSPARQL ~> ContentType("text/html") ~> ResponseString(body)
- }
- case GET(_) | HEAD(_) =>
- for {
- model <- r.get() failMap { x => NotFound }
- lang = representation match {
- case RDFRepr(l) => l
- case _ => Lang.default
- }
- } yield {
- req match {
- case GET(_) => Ok ~> ViaSPARQL ~> ContentType(lang.contentType) ~> ResponseModel(model, uri, lang)
- case HEAD(_) => Ok ~> ViaSPARQL ~> ContentType(lang.contentType)
+ val readWriteWebIntent = (req: Req) => {
+
+ val Authoritative(uri, representation) = req
+ val r: Resource = rm.resource(uri)
+ val res: Res = req match {
+ case GET(_) if representation == HTMLRepr => {
+ val source = Source.fromFile("src/main/resources/skin.html")("UTF-8")
+ val body = source.getLines.mkString("\n")
+ Ok ~> ViaSPARQL ~> ContentType("text/html") ~> ResponseString(body)
}
- }
- case PUT(_) & RequestLang(lang) if representation == DirectoryRepr => {
- for {
- bodyModel <- modelFromInputStream(Body.stream(req), uri, lang) failMap { t => BadRequest ~> ResponseString(t.getStackTraceString) }
- _ <- r.createDirectory(bodyModel) failMap { t => InternalServerError ~> ResponseString(t.getStackTraceString) }
- } yield Created
- }
- case PUT(_) & RequestLang(lang) =>
- for {
- bodyModel <- modelFromInputStream(Body.stream(req), uri, lang) failMap { t => BadRequest ~> ResponseString(t.getStackTraceString) }
- _ <- r.save(bodyModel) failMap { t => InternalServerError ~> ResponseString(t.getStackTraceString) }
- } yield Created
- case PUT(_) =>
- BadRequest ~> ResponseString("Content-Type MUST be one of: " + Lang.supportedAsString)
- case POST(_) & RequestContentType(ct) if Post.supportContentTypes contains ct => {
- Post.parse(Body.stream(req), uri, ct) match {
- case PostUnknown => {
- logger.info("Couldn't parse the request")
- BadRequest ~> ResponseString("You MUST provide valid content for given Content-Type: " + ct)
- }
- case PostUpdate(update) => {
- logger.info("SPARQL UPDATE:\n" + update.toString())
+ case GET(_) | HEAD(_) =>
for {
- model <- r.get() failMap { t => NotFound }
- // TODO: we should handle an error here
- _ = UpdateAction.execute(update, model)
- _ <- r.save(model) failMap { t => InternalServerError ~> ResponseString(t.getStackTraceString)}
- } yield Ok
- }
- case PostRDF(diffModel) => {
- logger.info("RDF content:\n" + diffModel.toString())
+ model <- r.get() failMap { x => NotFound }
+ lang = representation match {
+ case RDFRepr(l) => l
+ case _ => Lang.default
+ }
+ } yield {
+ req match {
+ case GET(_) => Ok ~> ViaSPARQL ~> ContentType(lang.contentType) ~> ResponseModel(model, uri, lang)
+ case HEAD(_) => Ok ~> ViaSPARQL ~> ContentType(lang.contentType)
+ }
+ }
+ case PUT(_) & RequestLang(lang) if representation == DirectoryRepr => {
for {
- model <- r.get() failMap { t => NotFound }
- // TODO: we should handle an error here
- _ = model.add(diffModel)
- _ <- r.save(model) failMap { t => InternalServerError ~> ResponseString(t.getStackTraceString)}
- } yield Ok
+ bodyModel <- modelFromInputStream(Body.stream(req), uri, lang) failMap { t => BadRequest ~> ResponseString(t.getStackTraceString) }
+ _ <- r.createDirectory(bodyModel) failMap { t => InternalServerError ~> ResponseString(t.getStackTraceString) }
+ } yield Created
}
- case PostQuery(query) => {
- logger.info("SPARQL Query:\n" + query.toString())
- lazy val lang = RequestLang(req) getOrElse Lang.default
+ case PUT(_) & RequestLang(lang) =>
for {
- model <- r.get() failMap { t => NotFound }
- } yield {
- val qe: QueryExecution = QueryExecutionFactory.create(query, model)
- query.getQueryType match {
- case SELECT =>
- Ok ~> ContentType("application/sparql-results+xml") ~> ResponseResultSet(qe.execSelect())
- case ASK =>
- Ok ~> ContentType("application/sparql-results+xml") ~> ResponseResultSet(qe.execAsk())
- case CONSTRUCT => {
- val result: Model = qe.execConstruct()
- Ok ~> ContentType(lang.contentType) ~> ResponseModel(model, uri, lang)
- }
- case DESCRIBE => {
- val result: Model = qe.execDescribe()
- Ok ~> ContentType(lang.contentType) ~> ResponseModel(model, uri, lang)
+ bodyModel <- modelFromInputStream(Body.stream(req), uri, lang) failMap { t => BadRequest ~> ResponseString(t.getStackTraceString) }
+ _ <- r.save(bodyModel) failMap { t => InternalServerError ~> ResponseString(t.getStackTraceString) }
+ } yield Created
+ case PUT(_) =>
+ BadRequest ~> ResponseString("Content-Type MUST be one of: " + Lang.supportedAsString)
+ case POST(_) & RequestContentType(ct) if Post.supportContentTypes contains ct => {
+ Post.parse(Body.stream(req), uri, ct) match {
+ case PostUnknown => {
+ logger.info("Couldn't parse the request")
+ BadRequest ~> ResponseString("You MUST provide valid content for given Content-Type: " + ct)
+ }
+ case PostUpdate(update) => {
+ logger.info("SPARQL UPDATE:\n" + update.toString())
+ for {
+ model <- r.get() failMap { t => NotFound }
+ // TODO: we should handle an error here
+ _ = UpdateAction.execute(update, model)
+ _ <- r.save(model) failMap { t => InternalServerError ~> ResponseString(t.getStackTraceString)}
+ } yield Ok
+ }
+ case PostRDF(diffModel) => {
+ logger.info("RDF content:\n" + diffModel.toString())
+ for {
+ model <- r.get() failMap { t => NotFound }
+ // TODO: we should handle an error here
+ _ = model.add(diffModel)
+ _ <- r.save(model) failMap { t => InternalServerError ~> ResponseString(t.getStackTraceString)}
+ } yield Ok
+ }
+ case PostQuery(query) => {
+ logger.info("SPARQL Query:\n" + query.toString())
+ lazy val lang = RequestLang(req) getOrElse Lang.default
+ for {
+ model <- r.get() failMap { t => NotFound }
+ } yield {
+ val qe: QueryExecution = QueryExecutionFactory.create(query, model)
+ query.getQueryType match {
+ case SELECT =>
+ Ok ~> ContentType("application/sparql-results+xml") ~> ResponseResultSet(qe.execSelect())
+ case ASK =>
+ Ok ~> ContentType("application/sparql-results+xml") ~> ResponseResultSet(qe.execAsk())
+ case CONSTRUCT => {
+ val result: Model = qe.execConstruct()
+ Ok ~> ContentType(lang.contentType) ~> ResponseModel(model, uri, lang)
+ }
+ case DESCRIBE => {
+ val result: Model = qe.execDescribe()
+ Ok ~> ContentType(lang.contentType) ~> ResponseModel(model, uri, lang)
+ }
+ }
}
}
}
}
+ case POST(_) =>
+ BadRequest ~> ResponseString("Content-Type MUST be one of: " + Post.supportedAsString)
+ case _ => MethodNotAllowed ~> Allow("GET", "PUT", "POST")
}
+ res
}
- case POST(_) =>
- BadRequest ~> ResponseString("Content-Type MUST be one of: " + Post.supportedAsString)
- case _ => MethodNotAllowed ~> Allow("GET", "PUT", "POST")
- }
- }
- }
-
- }
+
+
+
+
}
\ No newline at end of file