1. As POSTing ACLs to the filesystem gives them full URLs, it has become important to query the .meta.n3 using full URLs for the resources. This requires the AuthZ class to have access to the Manifest, in order to decide which way to find the full urls for a resource webid
authorHenry Story <henry.story@bblfish.net>
Fri, 28 Oct 2011 00:35:16 +0200
branchwebid
changeset 103 c0bf9b280888
parent 102 9ca474c333e8
child 107 3bb89aaaab51
1. As POSTing ACLs to the filesystem gives them full URLs, it has become important to query the .meta.n3 using full URLs for the resources. This requires the AuthZ class to have access to the Manifest, in order to decide which way to find the full urls for a resource
2. Added support for allowing any agent in ACL
3. Extended test suite to see if ACLs do in fact work
=> problem they don't currently, but this could be due to my not knowing how to get Java client to respond correctly to TLS renegotiation
4. added code for creating self signed certs - useful for tests
src/main/scala/auth/Authz.scala
src/main/scala/netty/ReadWriteWebNetty.scala
src/main/scala/plan.scala
src/test/scala/auth/CreateWebIDSpec.scala
--- a/src/main/scala/auth/Authz.scala	Thu Oct 27 01:06:22 2011 +0200
+++ b/src/main/scala/auth/Authz.scala	Fri Oct 28 00:35:16 2011 +0200
@@ -28,12 +28,12 @@
 import collection.JavaConversions._
 import javax.security.auth.Subject
 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}
-
+import com.hp.hpl.jena.rdf.model.{RDFNode, ResourceFactory}
+import org.w3.readwriteweb.{Authoritative, Resource, ResourceManager, WebCache}
 
 /**
  * @author hjs
@@ -75,9 +75,9 @@
 class NullAuthZ[Request,Response] extends AuthZ[Request,Response] {
   override def subject(req: Req): Option[Subject] = None
 
-  override def guard(m: Method, path: String): Guard = null
+  override def guard(m: Method, path: URL): Guard = null
 
-  override def protect(in: Req=>Res) = in
+  override def protect(in: Req=>Res)(implicit  m: Manifest[Request]) = in
 }
 
 
@@ -85,8 +85,9 @@
   type Req = HttpRequest[Request]
   type Res = ResponseFunction[Response]
 
-  def protect(in: Req=>Res): Req=>Res =  {
-      case req @ HttpMethod(method) & Path(path) if guard(method, path).allow(() => subject(req)) => in(req)
+
+  def protect(in: Req=>Res)(implicit  m: Manifest[Request]): Req=>Res =  {
+      case req @ HttpMethod(method) & Authoritative(url,_) if guard(method, url).allow(() => subject(req)) => in(req)
       case _ => Unauthorized
     }
   
@@ -94,9 +95,9 @@
   protected def subject(req: Req): Option[Subject]
 
   /** create the guard defined in subclass */
-  protected def guard(m: Method, path: String): Guard
+  protected def guard(m: Method, path: URL): Guard
 
-  abstract class Guard(m: Method, path: String) {
+  abstract class Guard(m: Method, url: URL) {
 
     /**
      * verify if the given request is authorized
@@ -120,6 +121,7 @@
 
   object RDFGuard {
     val acl = "http://www.w3.org/ns/auth/acl#"
+    val foafAgent = ResourceFactory.createResource("http://xmlns.com/foaf/0.1/Agent")
     val Read = acl+"Read"
     val Write = acl+"Write"
     val Control = acl+"Control"
@@ -135,23 +137,21 @@
     		  }""")
   }
 
-  def guard(method: Method, path: String) = new Guard(method, path) {
+  def guard(method: Method, url: URL) = new Guard(method, url) {
     import RDFGuard._
     import org.w3.readwriteweb.util.wrapValidation
     import org.w3.readwriteweb.util.ValidationW
 
-    lazy val dir = path.substring(0,path.lastIndexOf('/')+1) // we assume it always starts with /
 
 
     def allow(subj: () => Option[Subject]) = {
-      val resurl = "file://local"+dir + ".meta.n3"
-      val r: Resource = rm.resource(new URL(resurl))
+      val r: Resource = rm.resource(new URL(url,".meta.n3"))
       val res: ValidationW[Boolean,Boolean] = for {
         model <- r.get() failMap { x => true }
       } yield {
         val initialBinding = new QuerySolutionMap();
-        initialBinding.add("res", model.createResource("file://local"+path))
-        val qe: QueryExecution = QueryExecutionFactory.create(selectQuery, model, initialBinding)
+        initialBinding.add("res", model.createResource(url.toString))  //todo: this will work only if the files are described with relative URLs
+        val qe = QueryExecutionFactory.create(selectQuery, model, initialBinding)
         val agentsAllowed = try {
           val exec = qe.execSelect()
           val res = for (qs <- exec) yield {
@@ -161,15 +161,16 @@
               case Control => List(POST)
               case _ => List(GET, PUT, POST, DELETE) //nothing everything is allowed
             }
-            if (methods.contains(method)) Some(Pair(qs.get("agent"), qs.get("group")))
+            if (methods.contains(method)) Some((qs.get("agent"), qs.get("group")))
             else None
           }
           res.flatten.toList
         } finally {
           qe.close()
         }
-        if (agentsAllowed.size>0) {
-          subj() match {
+        if (agentsAllowed.size > 0) {
+          if (agentsAllowed.exists( pair =>  pair._2 == foafAgent )) true
+          else subj() match {
             case Some(s) => agentsAllowed.exists{ 
               p =>  s.getPrincipals(classOf[WebIdPrincipal]).
                 exists(id=> {
--- a/src/main/scala/netty/ReadWriteWebNetty.scala	Thu Oct 27 01:06:22 2011 +0200
+++ b/src/main/scala/netty/ReadWriteWebNetty.scala	Fri Oct 28 00:35:16 2011 +0200
@@ -31,7 +31,7 @@
 import unfiltered.netty.{ServerErrorResponse, ReceivedMessage, cycle}
 
 /**
- * ReadWrite Web for Netty server, allowing content renegotiation
+ * ReadWrite Web for Netty server, allowing TLS renegotiation
  *
  * @author hjs
  * @created: 21/10/2011
--- a/src/main/scala/plan.scala	Thu Oct 27 01:06:22 2011 +0200
+++ b/src/main/scala/plan.scala	Fri Oct 28 00:35:16 2011 +0200
@@ -53,7 +53,7 @@
    * many of which may want different authorization implementations )
    */
   def intent : Cycle.Intent[Req,Res] = {
-      case req @ Path(path) if path startsWith rm.basePath => authz.protect(rwwIntent)(req)
+      case req @ Path(path) if path startsWith rm.basePath => authz.protect(rwwIntent)(manif)(req)
   }
 
   /**
--- a/src/test/scala/auth/CreateWebIDSpec.scala	Thu Oct 27 01:06:22 2011 +0200
+++ b/src/test/scala/auth/CreateWebIDSpec.scala	Fri Oct 28 00:35:16 2011 +0200
@@ -28,13 +28,46 @@
 import dispatch._
 import java.io.File
 import org.apache.http.conn.scheme.Scheme
-import javax.net.ssl.{X509TrustManager, TrustManager}
 import java.lang.String
 import java.security.cert.{CertificateFactory, X509Certificate}
 import java.security._
 import interfaces.RSAPublicKey
 import org.w3.readwriteweb.{RDFXML, TURTLE}
-import java.net.URL
+import java.net.{Socket, URL}
+import scala.collection.{mutable, immutable}
+import javax.net.ssl._
+
+
+/**
+ * A key manager that can contain multiple keys, but where the client can take one of a number of identities
+ * One at a time - so this is not sychronised. It also assumes that the server will accept all CAs, which in
+ * these test cases it does.
+ */
+class FlexiKeyManager extends X509ExtendedKeyManager {
+  val keys = mutable.Map[String, Pair[Array[X509Certificate],PrivateKey]]()
+  
+  def addClientCert(alias: String,certs: Array[X509Certificate], privateKey: PrivateKey) {
+    keys.put(alias,Pair(certs,privateKey))
+  }
+  
+  var currentId: String = null
+  
+  def setId(alias: String) { currentId = if (keys.contains(alias)) alias else null }
+  
+  def getClientAliases(keyType: String, issuers: Array[Principal]) = if (currentId!=null) Array(currentId) else null
+  
+  def chooseClientAlias(keyType: Array[String], issuers: Array[Principal], socket: Socket) = currentId
+
+  def getServerAliases(keyType: String, issuers: Array[Principal]) = null
+
+  def chooseServerAlias(keyType: String, issuers: Array[Principal], socket: Socket) = ""
+
+  def getCertificateChain(alias: String) = keys.get(alias) match { case Some(certNKey) => certNKey._1; case None => null}
+
+  def getPrivateKey(alias: String) = keys.get(alias).map(ck=>ck._2).getOrElse(null)
+
+  override def chooseEngineClientAlias(keyType: Array[String], issuers: Array[Principal], engine: SSLEngine): String = currentId
+}
 
 /**
  * @author hjs
@@ -52,17 +85,21 @@
   lazy val lambdaDir = new File(directory,"Lambda")
   lazy val lambdaMeta = new File(lambdaDir,".meta.n3")
 
-{
+
+ val keyManager = new FlexiKeyManager();
+
   val  sslContext = javax.net.ssl.SSLContext.getInstance("TLS");
-  sslContext.init(null, Array[TrustManager](new X509TrustManager {
+  sslContext.init(Array(keyManager.asInstanceOf[KeyManager]), Array[TrustManager](new X509TrustManager {
     def checkClientTrusted(chain: Array[X509Certificate], authType: String) {}
     def checkServerTrusted(chain: Array[X509Certificate], authType: String) {}
     def getAcceptedIssuers = Array[X509Certificate]()
   }),null); // we are not trying to test our trust of localhost server
-  val sf = new org.apache.http.conn.ssl.SSLSocketFactory(sslContext)
-  val  scheme = new Scheme("https", sf, 443);
-  Http.client.getConnectionManager.getSchemeRegistry.register(scheme)
-}
+  {
+   import org.apache.http.conn.ssl._
+   val sf = new SSLSocketFactory(sslContext,SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER)
+   val  scheme = new Scheme("https", 443, sf);
+   Http.client.getConnectionManager.getSchemeRegistry.register(scheme)
+  }
 
 
   val foaf = """
@@ -87,14 +124,24 @@
        }
   """
 
+  val updateFriend = """
+       PREFIX foaf: <http://xmlns.com/foaf/0.1/>
+       PREFIX : <#>
+       INSERT DATA {
+          :j1 foaf:knows <%s> .
+       }
+  """
+
   val rsagen = KeyPairGenerator.getInstance("RSA")
   rsagen.initialize(512)
   val rsaKP = rsagen.generateKeyPair()
   val certFct = CertificateFactory.getInstance("X.509")
   val webID = new URL(webidProfile.secure.to_uri + "#me")
-  val testCert = X509Cert.generate_self_signed("CN=RoboTester, OU=DIG, O=W3C", rsaKP, 1, webID)
+  val testCert = X509Cert.generate_self_signed("CN=JoeLambda, OU=DIG, O=W3C", rsaKP, 1, webID)
 
   val testCertPk = testCert.getPublicKey.asInstanceOf[RSAPublicKey]
+
+  keyManager.addClientCert("JoeLambda",Array(testCert),rsaKP.getPrivate)
   
   "PUTing nothing on /people/" should {
        "return a 201" in {
@@ -148,12 +195,18 @@
 
   val aclRestriction = """
   @prefix acl: <http://www.w3.org/ns/auth/acl#> .
+  @prefix foaf: <http://xmlns.com/foaf/0.1/> .
   @prefix : <#> .
 
   :a1 a acl:Authorization;
-     acl:accessTo <foaf.n3>;
+     acl:accessTo <Joe>;
+     acl:mode acl:Write;
+     acl:agent <%s> .
+
+  :allRead a acl:Authorization;
+     acl:accessTo <Joe>;
      acl:mode acl:Read;
-     acl:agent <%s> .
+     acl:agentClass foaf:Agent .
   """
 
 
@@ -166,14 +219,45 @@
     "create a resource on disk" in {
        lambdaMeta must be file
     }
-    "make the initial resource inaccessible to anyone other than the user" in {
-      val httpCode = Http.when(_ == 401)(webidProfile.secure.get get_statusCode)
+    
+    "everybody can still read the profile" in {
+      keyManager.setId(null)
+      val model = Http(webidProfile.secure as_model(baseURI(webidProfile.secure), RDFXML))
+      model.size() must_== 7
+    }
+    
+    "no one other than the user can change the profile" in {
+      val httpCode = Http.when(_ == 401)(webidProfile.secure.put(TURTLE, foaf) get_statusCode)
       httpCode must_== 401
     }
-//    "access it as the user" in {
-//    ok so here we have to set the client certificate for the connection only.
-//      Http.client.getConnectionManager.
-//    }
+
+    "access it as the user - allow him to add a friend" in {
+      keyManager.setId("JoeLambda")
+//      val scon =webidProfile.secure.to_uri.toURL.openConnection().asInstanceOf[HttpsURLConnection]
+//      scon.setSSLSocketFactory(sslContext.getSocketFactory)
+//      scon.setRequestProperty("Content-Type",TURTLE.contentType)
+//      scon.setRequestMethod("POST")
+//      val msg = updateFriend.format("http://bblfish.net/#hjs").getBytes("UTF-8")
+//      scon.setRequestProperty("Content-Length",msg.length.toString)
+//      scon.setDoOutput(true)
+//      scon.setDoInput(true)
+//
+//      val out = scon.getOutputStream
+//      out.write(msg)
+//      out.flush()
+//      out.close()
+//      scon.connect()
+//
+//      val httpCode = scon.getResponseCode
+
+      val httpCode = Http( webidProfile.secure.put(TURTLE, updateFriend.format("http://bblfish.net/#hjs")) get_statusCode )
+      httpCode must_== 201
+    }
+
+    "and so have one more relation in the foaf" in {
+      val model = Http(webidProfile.secure as_model(baseURI(webidProfile.secure), RDFXML))
+      model.size() must_== 8
+    }
 
   }