diff --git a/akka-kernel/pom.xml b/akka-kernel/pom.xml
index 136d94d8ff..9cdf9fa53a 100644
--- a/akka-kernel/pom.xml
+++ b/akka-kernel/pom.xml
@@ -42,6 +42,11 @@
se.scalablesolutions.akka
0.6
+
+ akka-security
+ se.scalablesolutions.akka
+ 0.6
+
diff --git a/akka-kernel/src/main/scala/AkkaServlet.scala b/akka-kernel/src/main/scala/AkkaServlet.scala
index 15862ff43e..dfb70c3445 100755
--- a/akka-kernel/src/main/scala/AkkaServlet.scala
+++ b/akka-kernel/src/main/scala/AkkaServlet.scala
@@ -35,10 +35,7 @@ class AkkaServlet extends ServletContainer with AtmosphereServletProcessor with
val configurators = ConfiguratorRepository.getConfigurators
rc.getClasses.addAll(configurators.flatMap(_.getComponentInterfaces))
- log.info("Starting AkkaServlet with ResourceFilters: " + rc.getProperty("com.sun.jersey.spi.container.ResourceFilters"));
- rc.getProperties.put("com.sun.jersey.spi.container.ResourceFilters", "org.atmosphere.core.AtmosphereFilter")
- //rc.getFeatures.put("com.sun.jersey.config.feature.Redirect", true)
- //rc.getFeatures.put("com.sun.jersey.config.feature.ImplicitViewables",true)
+ rc.getProperties.put("com.sun.jersey.spi.container.ResourceFilters", akka.Config.config.getString("akka.rest.filters").getOrElse(""))
wa.initiate(rc, new ActorComponentProviderFactory(configurators))
}
diff --git a/akka-samples-security/pom.xml b/akka-samples-security/pom.xml
new file mode 100644
index 0000000000..d870bdac17
--- /dev/null
+++ b/akka-samples-security/pom.xml
@@ -0,0 +1,83 @@
+
+ 4.0.0
+
+ akka-samples-security
+ Akka Sample Security Module
+
+ jar
+
+
+ akka
+ se.scalablesolutions.akka
+ 0.6
+ ../pom.xml
+
+
+
+
+ akka-kernel
+ se.scalablesolutions.akka
+ 0.6
+
+
+ akka-util-java
+ se.scalablesolutions.akka
+ 0.6
+
+
+ akka-util
+ se.scalablesolutions.akka
+ 0.6
+
+
+ akka-actors
+ se.scalablesolutions.akka
+ 0.6
+
+
+ akka-security
+ se.scalablesolutions.akka
+ 0.6
+
+
+ akka-persistence
+ se.scalablesolutions.akka
+ 0.6
+
+
+ javax.ws.rs
+ jsr311-api
+ 1.0
+
+
+ javax.annotation
+ jsr250-api
+ 1.0
+
+
+
+
+
+ src/main/scala
+
+
+ maven-antrun-plugin
+
+
+ install
+
+
+
+
+
+
+ run
+
+
+
+
+
+
+
diff --git a/akka-samples-security/src/main/scala/SimpleService.scala b/akka-samples-security/src/main/scala/SimpleService.scala
new file mode 100644
index 0000000000..a3627586e6
--- /dev/null
+++ b/akka-samples-security/src/main/scala/SimpleService.scala
@@ -0,0 +1,87 @@
+/**
+ * Copyright (C) 2009 Scalable Solutions.
+ */
+
+package sample.secure
+
+import _root_.se.scalablesolutions.akka.state.{TransactionalState,PersistentState, CassandraStorageConfig}
+import _root_.se.scalablesolutions.akka.actor.{SupervisorFactory, Actor}
+import _root_.se.scalablesolutions.akka.config.ScalaConfig._
+import _root_.se.scalablesolutions.akka.util.Logging
+import _root_.se.scalablesolutions.akka.security.{DigestAuthenticationActor, UserInfo}
+import _root_.javax.annotation.security.{DenyAll,PermitAll,RolesAllowed}
+import javax.ws.rs.{GET, POST, Path, Produces, Consumes}
+
+class Boot {
+ object factory extends SupervisorFactory {
+ override def getSupervisorConfig: SupervisorConfig = {
+ SupervisorConfig(
+ RestartStrategy(OneForOne, 3, 100),
+ Supervise(
+ new SimpleAuthenticationService,
+ LifeCycle(Permanent, 100)) ::
+ Supervise(
+ new SecureService,
+ LifeCycle(Permanent, 100)):: Nil)
+ }
+ }
+
+ val supervisor = factory.newSupervisor
+ supervisor.startSupervisor
+}
+
+/*
+ * In akka.conf you can set the FQN of any AuthenticationActor of your wish, under the property name: akka.rest.authenticator
+ */
+class SimpleAuthenticationService extends DigestAuthenticationActor
+{
+ //If you want to have a distributed nonce-map, you can use something like below,
+ //don't forget to configure your standalone Cassandra instance
+ //
+ //makeTransactionRequired
+ //override def mkNonceMap = PersistentState.newMap(CassandraStorageConfig()).asInstanceOf[scala.collection.mutable.Map[String,Long]]
+
+ //Use an in-memory nonce-map as default
+ override def mkNonceMap = new scala.collection.mutable.HashMap[String,Long]
+ //Change this to whatever you want
+ override def realm = "test"
+
+ //Dummy method that allows you to log on with whatever username with the password "bar"
+ override def userInfo(username : String) : Option[UserInfo] = Some(UserInfo(username,"bar","ninja" :: "chef" :: Nil))
+}
+
+/**
+ * This is merely a secured version of the scala-sample
+ *
+ * The interesting part is
+ * @RolesAllowed
+ * @PermitAll
+ * @DenyAll
+ */
+
+@Path("/securecount")
+class SecureService extends Actor with Logging {
+ makeTransactionRequired
+
+ case object Tick
+ private val KEY = "COUNTER";
+ private var hasStartedTicking = false;
+ private val storage = PersistentState.newMap(CassandraStorageConfig())
+
+ @GET
+ @Produces(Array("text/html"))
+ @RolesAllowed(Array("chef"))
+ def count = (this !! Tick).getOrElse(Error in counter)
+
+ override def receive: PartialFunction[Any, Unit] = {
+ case Tick => if (hasStartedTicking) {
+ val counter = storage.get(KEY).get.asInstanceOf[Integer].intValue
+ storage.put(KEY, new Integer(counter + 1))
+ reply(Tick:{counter + 1})
+ } else {
+ storage.put(KEY, new Integer(0))
+ hasStartedTicking = true
+ reply(Tick: 0)
+ }
+ }
+}
\ No newline at end of file
diff --git a/akka-security/pom.xml b/akka-security/pom.xml
new file mode 100644
index 0000000000..8862b10f18
--- /dev/null
+++ b/akka-security/pom.xml
@@ -0,0 +1,65 @@
+
+ 4.0.0
+
+ akka-security
+ Akka Security Module
+
+ jar
+
+
+ akka
+ se.scalablesolutions.akka
+ 0.6
+ ../pom.xml
+
+
+
+
+ org.scala-lang
+ scala-library
+ 2.7.5
+
+
+
+ akka-actors
+ se.scalablesolutions.akka
+ 0.6
+
+
+ akka-persistence
+ se.scalablesolutions.akka
+ 0.6
+
+
+ akka-util
+ se.scalablesolutions.akka
+ 0.6
+
+
+ javax.annotation
+ jsr250-api
+ 1.0
+
+
+ com.sun.jersey
+ jersey-server
+ 1.1.1-ea
+
+
+ javax.ws.rs
+ jsr311-api
+ 1.0
+
+
+ net.liftweb
+ lift-util
+ 1.1-SNAPSHOT
+
+
+
+
diff --git a/akka-security/src/main/scala/Security.scala b/akka-security/src/main/scala/Security.scala
new file mode 100644
index 0000000000..b2f26e1bfe
--- /dev/null
+++ b/akka-security/src/main/scala/Security.scala
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2007-2008 WorldWide Conferencing, LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ */
+
+/*
+ * AKKA AAS (Authentication and Authorization Service)
+ * Rework of lift's (www.liftweb.com) HTTP Authentication module
+ * All cred to the Lift team (www.liftweb.com), especially David Pollak and Tim Perrett
+ */
+
+package se.scalablesolutions.akka.security
+
+import _root_.se.scalablesolutions.akka.actor.{Scheduler,Actor,ActorRegistry}
+import _root_.se.scalablesolutions.akka.state.{TransactionalState,PersistentStorageConfig}
+import _root_.se.scalablesolutions.akka.util.{Logging}
+
+import _root_.com.sun.jersey.api.model.AbstractMethod
+import _root_.com.sun.jersey.spi.container.{ResourceFilterFactory,ContainerRequest,ContainerRequestFilter,ContainerResponse,ContainerResponseFilter,ResourceFilter}
+import _root_.com.sun.jersey.core.util.Base64
+import _root_.javax.ws.rs.core.{SecurityContext,Context,Response}
+import _root_.javax.ws.rs.WebApplicationException
+import _root_.javax.annotation.security.{DenyAll,PermitAll,RolesAllowed}
+import _root_.java.security.Principal
+import _root_.java.util.concurrent.TimeUnit
+
+import _root_.net.liftweb.util.{SecurityHelpers, StringHelpers,IoHelpers}
+
+object Enc extends SecurityHelpers with StringHelpers with IoHelpers
+
+case object OK
+
+/**
+ * Authenticate represents a message to authenticate a request
+ */
+case class Authenticate(val req : ContainerRequest, val rolesAllowed : List[String])
+
+/**
+ * User info represents a sign-on with associated credentials/roles
+ */
+case class UserInfo(val username : String,val password : String,val roles : List[String])
+
+
+
+trait Credentials
+
+case class BasicCredentials(username : String, password : String) extends Credentials
+
+case class DigestCredentials(method: String,
+ userName: String,
+ realm: String,
+ nonce: String,
+ uri: String,
+ qop: String,
+ nc: String,
+ cnonce: String,
+ response: String,
+ opaque: String) extends Credentials
+
+/**
+ * Jersey Filter for invocation intercept and authorization/authentication
+ */
+class AkkaSecurityFilterFactory extends ResourceFilterFactory with Logging {
+
+ class Filter(actor : Actor,rolesAllowed : Option[List[String]]) extends ResourceFilter with ContainerRequestFilter with Logging {
+
+ override def getRequestFilter : ContainerRequestFilter = this
+ override def getResponseFilter : ContainerResponseFilter = null
+
+ /**
+ * Here's where the magic happens. The request is authenticated by
+ * sending a request for authentication to the configured authenticator actor
+ */
+ override def filter(request : ContainerRequest) : ContainerRequest =
+ rolesAllowed match {
+ case Some(roles) => {
+
+ val result : AnyRef = (authenticator !? Authenticate(request,roles))
+
+ result match {
+ case OK => request
+ case r if r.isInstanceOf[Response] =>
+ throw new WebApplicationException(r.asInstanceOf[Response])
+ case x => {
+ log.error("Authenticator replied with unexpected result: ",x);
+ throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR)
+ }
+ }
+ }
+ case None => throw new WebApplicationException(Response.Status.FORBIDDEN)
+ }
+ }
+
+ lazy val authenticatorFQN = akka.Config.config.getString("akka.rest.authenticator").getOrElse(throw new IllegalStateException("akka.rest.authenticator"))
+
+ /**
+ * Currently we always take the first, since there usually should be at most one authentication actor, but a round-robin
+ * strategy could be implemented in the future
+ */
+ def authenticator : Actor = ActorRegistry.actorsFor(authenticatorFQN).head
+
+ def mkFilter(roles : Option[List[String]]) : java.util.List[ResourceFilter] = java.util.Collections.singletonList(new Filter(authenticator,roles))
+
+ /**
+ * The create method is invoked for each resource, and we look for javax.annotation.security annotations
+ * and create the appropriate Filter configurations for each.
+ */
+ override def create(am : AbstractMethod) : java.util.List[ResourceFilter] = {
+
+ //DenyAll takes precedence
+ if (am.isAnnotationPresent(classOf[DenyAll]))
+ return mkFilter(None)
+
+ //Method-level RolesAllowed takes precedence
+ val ra = am.getAnnotation(classOf[RolesAllowed])
+
+ if (ra ne null)
+ return mkFilter(Some(ra.value.toList))
+
+ //PermitAll takes precedence over resource-level RolesAllowed annotation
+ if (am.isAnnotationPresent(classOf[PermitAll]))
+ return null;
+
+ //Last but not least, the resource-level RolesAllowed
+ val cra = am.getResource.getAnnotation(classOf[RolesAllowed])
+ if (cra ne null)
+ return mkFilter(Some(ra.value.toList))
+
+ return null;
+ }
+}
+
+/**
+ * AuthenticationActor is the super-trait for actors doing Http authentication
+ * It defines the common ground and the flow of execution
+ */
+trait AuthenticationActor[C <: Credentials] extends Actor with Logging
+{
+ type Req = ContainerRequest
+
+ //What realm does the authentication use?
+ def realm : String
+
+ //Creates a response to signal unauthorized
+ def unauthorized : Response
+
+ //Used to extract information from the request, returns None if no credentials found
+ def extractCredentials(r : Req) : Option[C]
+
+ //returns None is unverified
+ def verify(c : Option[C]) : Option[UserInfo]
+
+ //Contruct a new SecurityContext from the supplied parameters
+ def mkSecurityContext(r : Req, user : UserInfo) : SecurityContext
+
+ //This is the default security context factory
+ def mkDefaultSecurityContext(r : Req,u : UserInfo, scheme : String) : SecurityContext = {
+ val n = u.username
+ val p = new Principal { def getName = n }
+
+ new SecurityContext {
+ def getAuthenticationScheme = scheme
+ def getUserPrincipal = p
+ def isSecure = r.isSecure
+ def isUserInRole(role : String) = u.roles.exists(_ == role)
+ }
+ }
+
+ /**
+ * Responsible for the execution flow of authentication
+ *
+ * Credentials are extracted and verified from the request,
+ * and a se3curity context is created for the ContainerRequest
+ * this should ensure good integration with current Jersey security
+ */
+ protected val authenticate: PartialFunction[Any,Unit] = {
+ case Authenticate(req,roles) => {
+ verify(extractCredentials(req)) match {
+ case Some(u : UserInfo) => {
+
+ req.setSecurityContext(mkSecurityContext(req,u))
+
+ if(roles.exists(req.isUserInRole(_)))
+ reply(OK)
+ else
+ reply(Response.status(Response.Status.FORBIDDEN).build)
+ }
+ case _ => reply(unauthorized)
+ }
+ }
+ }
+
+ override def receive: PartialFunction[Any, Unit] = authenticate
+
+ //returns the string value of the "Authorization"-header of the request
+ def auth(r : Req) = r.getHeaderValue("Authorization")
+
+ //Turns the aforementioned header value into an option
+ def authOption(r : Req) : Option[String] = {
+ val a = auth(r)
+ if(a != null && a.length > 0) Some(a) else None
+ }
+}
+
+/**
+ * This trait implements the logic for Http Basic authentication
+ * mix this trait into a class to create an authenticator
+ * Don't forget to set the authenticator FQN in the rest-part of the akka config
+ */
+trait BasicAuthenticationActor extends AuthenticationActor[BasicCredentials]
+{
+ override def unauthorized =
+ Response.status(401).header("WWW-Authenticate","Basic realm=\"" + realm + "\"").build
+
+ override def extractCredentials(r : Req) : Option[BasicCredentials] = {
+ val a = r.getHeaderValue("Authorization")
+ new String(Base64.decode(a.substring(6,a.length).getBytes)).split(":").toList match {
+ case userName :: password :: _ => Some(BasicCredentials(userName, password))
+ case userName :: Nil => Some(BasicCredentials(userName, ""))
+ case _ => None
+ }
+ }
+
+ override def mkSecurityContext(r : Req,u : UserInfo) : SecurityContext =
+ mkDefaultSecurityContext(r,u,SecurityContext.BASIC_AUTH)
+}
+
+/**
+ * This trait implements the logic for Http Digest authentication
+ * mix this trait into a class to create an authenticator
+ * Don't forget to set the authenticator FQN in the rest-part of the akka config
+ */
+trait DigestAuthenticationActor extends AuthenticationActor[DigestCredentials]
+{
+ import Enc._
+
+ private object InvalidateNonces
+
+ //Holds the generated nonces for the specified validity period
+ val nonceMap = mkNonceMap
+
+ //Discards old nonces
+ protected val invalidateNonces: PartialFunction[Any,Unit] = {
+ case InvalidateNonces =>
+ {
+ val ts = System.currentTimeMillis
+
+ nonceMap.retain((k,v) => (ts - v) < nonceValidityPeriod)
+ }
+
+ case e => log.info("Don't know what to do with: " + e)
+ }
+
+ //Schedule the invalidation of nonces
+ Scheduler.schedule(this, InvalidateNonces, noncePurgeInterval, noncePurgeInterval, TimeUnit.MILLISECONDS )
+
+ //authenticate or invalidate nonces
+ override def receive: PartialFunction[Any, Unit] = authenticate orElse invalidateNonces
+
+ override def unauthorized : Response =
+ {
+ val nonce = randomString(64);
+ nonceMap.put(nonce,System.currentTimeMillis)
+ unauthorized(nonce,"auth",randomString(64))
+ }
+
+ def unauthorized(nonce : String, qop : String, opaque : String) : Response =
+ {
+ Response.status(401).header("WWW-Authenticate",
+ "Digest realm=\"" + realm + "\", " +
+ "qop=\"" + qop + "\", " +
+ "nonce=\"" + nonce + "\", " +
+ "opaque=\"" + opaque + "\"").build
+ }
+
+ //Tests wether the specified credentials are valid
+ def validate(auth: DigestCredentials,user : UserInfo) : Boolean = {
+ def h(s : String) = hexEncode(md5(s.getBytes("UTF-8")))
+
+ val ha1 = h(auth.userName + ":" + auth.realm + ":" + user.password)
+ val ha2 = h(auth.method + ":" + auth.uri)
+
+ val response = h(ha1 + ":" + auth.nonce + ":" +
+ auth.nc + ":" + auth.cnonce + ":" +
+ auth.qop + ":" + ha2)
+
+ (response == auth.response) && (nonceMap.getOrElse(auth.nonce, -1) != -1)
+ }
+
+ override def verify(odc : Option[DigestCredentials]) : Option[UserInfo] = odc match {
+ case Some(dc) => {
+ userInfo(dc.userName) match {
+ case Some(u) if validate(dc,u) =>
+ nonceMap.get(dc.nonce).map( t => (System.currentTimeMillis - t) < nonceValidityPeriod ).map(_ => u)
+ case _ => None
+ }
+ }
+ case _ => None
+ }
+
+ override def extractCredentials(r : Req) : Option[DigestCredentials] =
+ {
+ authOption(r).map( s => {
+ val ? = splitNameValuePairs(s.substring(7,s.length ))
+
+ DigestCredentials(r.getMethod.toUpperCase, ?("username"), ?("realm"), ?("nonce"),
+ ?("uri"), ?("qop"), ?("nc"),
+ ?("cnonce"), ?("response"), ?("opaque"))
+ })
+ }
+
+ override def mkSecurityContext(r : Req,u : UserInfo) : SecurityContext =
+ mkDefaultSecurityContext(r,u,SecurityContext.DIGEST_AUTH)
+
+ //Mandatory overrides
+ def userInfo(username : String) : Option[UserInfo]
+
+ def mkNonceMap : scala.collection.mutable.Map[String,Long]
+
+ //Optional overrides
+ def nonceValidityPeriod = 60*1000//ms
+ def noncePurgeInterval = 2*60*1000 //ms
+}
\ No newline at end of file
diff --git a/config/akka-reference.conf b/config/akka-reference.conf
index 569f9fd9c5..cabe677c11 100644
--- a/config/akka-reference.conf
+++ b/config/akka-reference.conf
@@ -17,7 +17,7 @@
version = "0.6"
- boot = ["sample.java.Boot", "sample.scala.Boot"] # FQN to the class doing initial active object/actor
+ boot = ["sample.java.Boot", "sample.scala.Boot", "sample.secure.Boot"] # FQN to the class doing initial active object/actor
# supervisor bootstrap, should be defined in default constructor
timeout = 5000 # default timeout for future based invocations
@@ -44,6 +44,8 @@
service = on
hostname = "localhost"
port = 9998
+ filters = "se.scalablesolutions.akka.security.AkkaSecurityFilterFactory;org.atmosphere.core.AtmosphereFilter"
+ authenticator = "sample.secure.SimpleAuthenticationService"
diff --git a/pom.xml b/pom.xml
index 0fa7b50324..688563fc0f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -25,11 +25,13 @@
akka-rest
akka-camel
akka-amqp
+ akka-security
akka-kernel
akka-fun-test-java
akka-samples-scala
akka-samples-lift
akka-samples-java
+ akka-samples-security