Merge branch 'master' into 911-krasserm

This commit is contained in:
Martin Krasser 2011-06-07 11:59:41 +02:00
commit df62230bfe
18 changed files with 287 additions and 1175 deletions

View file

@ -11,9 +11,10 @@ import akka.testing._
import akka.util.duration._
import akka.testing.Testing.sleepFor
import akka.config.Supervision.{ OneForOneStrategy }
import akka.actor._
import akka.dispatch.Future
import java.util.concurrent.{ TimeUnit, CountDownLatch }
import java.lang.IllegalStateException
import akka.util.ReflectiveAccess
object ActorRefSpec {
@ -68,26 +69,189 @@ object ActorRefSpec {
}
}
}
class OuterActor(val inner: ActorRef) extends Actor {
def receive = {
case "self" self reply self
case x inner forward x
}
}
class FailingOuterActor(val inner: ActorRef) extends Actor {
val fail = new InnerActor
def receive = {
case "self" self reply self
case x inner forward x
}
}
class FailingInheritingOuterActor(_inner: ActorRef) extends OuterActor(_inner) {
val fail = new InnerActor
}
class InnerActor extends Actor {
def receive = {
case "innerself" self reply self
case other self reply other
}
}
class FailingInnerActor extends Actor {
val fail = new InnerActor
def receive = {
case "innerself" self reply self
case other self reply other
}
}
class FailingInheritingInnerActor extends InnerActor {
val fail = new InnerActor
}
}
class ActorRefSpec extends WordSpec with MustMatchers {
import ActorRefSpec._
import akka.actor.ActorRefSpec._
"An ActorRef" must {
"not allow Actors to be created outside of an actorOf" in {
intercept[akka.actor.ActorInitializationException] {
new Actor { def receive = { case _ } }
fail("shouldn't get here")
}
intercept[akka.actor.ActorInitializationException] {
val a = Actor.actorOf(new Actor {
Actor.actorOf(new Actor {
val nested = new Actor { def receive = { case _ } }
def receive = { case _ }
}).start()
fail("shouldn't get here")
}
def refStackMustBeEmpty = Actor.actorRefInCreation.get.headOption must be === None
refStackMustBeEmpty
intercept[akka.actor.ActorInitializationException] {
Actor.actorOf(new FailingOuterActor(Actor.actorOf(new InnerActor).start)).start()
}
refStackMustBeEmpty
intercept[akka.actor.ActorInitializationException] {
Actor.actorOf(new OuterActor(Actor.actorOf(new FailingInnerActor).start)).start()
}
refStackMustBeEmpty
intercept[akka.actor.ActorInitializationException] {
Actor.actorOf(new FailingInheritingOuterActor(Actor.actorOf(new InnerActor).start)).start()
}
refStackMustBeEmpty
intercept[akka.actor.ActorInitializationException] {
Actor.actorOf(new FailingOuterActor(Actor.actorOf(new FailingInheritingInnerActor).start)).start()
}
refStackMustBeEmpty
intercept[akka.actor.ActorInitializationException] {
Actor.actorOf(new FailingInheritingOuterActor(Actor.actorOf(new FailingInheritingInnerActor).start)).start()
}
refStackMustBeEmpty
intercept[akka.actor.ActorInitializationException] {
Actor.actorOf(new FailingInheritingOuterActor(Actor.actorOf(new FailingInnerActor).start)).start()
}
refStackMustBeEmpty
intercept[akka.actor.ActorInitializationException] {
Actor.actorOf(new OuterActor(Actor.actorOf(new InnerActor {
val a = new InnerActor
}).start)).start()
}
refStackMustBeEmpty
intercept[akka.actor.ActorInitializationException] {
Actor.actorOf(new FailingOuterActor(Actor.actorOf(new FailingInheritingInnerActor).start)).start()
}
refStackMustBeEmpty
intercept[akka.actor.ActorInitializationException] {
Actor.actorOf(new OuterActor(Actor.actorOf(new FailingInheritingInnerActor).start)).start()
}
refStackMustBeEmpty
intercept[akka.actor.ActorInitializationException] {
Actor.actorOf(new OuterActor(Actor.actorOf({ new InnerActor; new InnerActor }).start)).start()
}
refStackMustBeEmpty
(intercept[java.lang.IllegalStateException] {
Actor.actorOf(new OuterActor(Actor.actorOf({ throw new IllegalStateException("Ur state be b0rked"); new InnerActor }).start)).start()
}).getMessage must be === "Ur state be b0rked"
refStackMustBeEmpty
}
"be serializable using Java Serialization on local node" in {
val a = Actor.actorOf[InnerActor].start
import java.io._
val baos = new ByteArrayOutputStream(8192 * 32)
val out = new ObjectOutputStream(baos)
out.writeObject(a)
out.flush
out.close
val in = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray))
val readA = in.readObject
a.isInstanceOf[LocalActorRef] must be === true
readA.isInstanceOf[LocalActorRef] must be === true
(readA eq a) must be === true
}
"must throw exception on deserialize if not present in local registry and remoting is not enabled" in {
ReflectiveAccess.RemoteModule.isEnabled must be === false
val a = Actor.actorOf[InnerActor].start
val inetAddress = ReflectiveAccess.RemoteModule.configDefaultAddress
val expectedSerializedRepresentation = SerializedActorRef(
a.uuid,
a.address,
inetAddress.getAddress.getHostAddress,
inetAddress.getPort,
a.timeout)
Actor.registry.unregister(a)
import java.io._
val baos = new ByteArrayOutputStream(8192 * 32)
val out = new ObjectOutputStream(baos)
out.writeObject(a)
out.flush
out.close
val in = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray))
(intercept[java.lang.IllegalStateException] {
in.readObject
}).getMessage must be === "Trying to deserialize ActorRef (" + expectedSerializedRepresentation + ") but it's not found in the local registry and remoting is not enabled!"
}
"support nested actorOfs" in {
@ -102,6 +266,17 @@ class ActorRefSpec extends WordSpec with MustMatchers {
(a ne nested) must be === true
}
"support advanced nested actorOfs" in {
val a = Actor.actorOf(new OuterActor(Actor.actorOf(new InnerActor).start)).start
val inner = (a !! "innerself").get
(a !! a).get must be(a)
(a !! "self").get must be(a)
inner must not be a
(a !! "msg").get must be === "msg"
}
"support reply via channel" in {
val serverRef = Actor.actorOf[ReplyActor].start()
val clientRef = Actor.actorOf(new SenderActor(serverRef)).start()

View file

@ -513,8 +513,13 @@ trait Actor {
"\n\t\t'val actor = Actor.actorOf(new MyActor(..))'")
val ref = refStack.head
Actor.actorRefInCreation.set(refStack.pop)
Some(ref)
if (ref eq null)
throw new ActorInitializationException("Trying to create an instance of an Actor outside of a wrapping 'actorOf'")
else {
Actor.actorRefInCreation.set(refStack.push(null)) //Push a null marker so any subsequent calls to new Actor doesn't reuse this actor ref
Some(ref)
}
}
/*
@ -692,13 +697,4 @@ private[actor] class AnyOptionAsTypedOption(anyOption: Option[Any]) {
* ClassCastException and return None in that case.
*/
def asSilently[T: Manifest]: Option[T] = narrowSilently[T](anyOption)
}
/**
* Marker interface for proxyable actors (such as typed actor).
*
* @author <a href="http://jonasboner.com">Jonas Bon&#233;r</a>
*/
trait Proxyable {
private[actor] def swapProxiedActor(newInstance: Actor)
}
}

View file

@ -19,6 +19,7 @@ import java.util.{ Map ⇒ JMap }
import scala.reflect.BeanProperty
import scala.collection.immutable.Stack
import scala.annotation.tailrec
import java.lang.IllegalStateException
private[akka] object ActorRefInternals {
@ -85,14 +86,14 @@ abstract class Channel[T] {
*
* @author <a href="http://jonasboner.com">Jonas Bon&#233;r</a>
*/
trait ActorRef extends ActorRefShared with java.lang.Comparable[ActorRef] { scalaRef: ScalaActorRef
trait ActorRef extends ActorRefShared with java.lang.Comparable[ActorRef] with Serializable { scalaRef: ScalaActorRef
// Only mutable for RemoteServer in order to maintain identity across nodes
@volatile
protected[akka] var _uuid = newUuid
@volatile
protected[this] var _status: ActorRefInternals.StatusType = ActorRefInternals.UNSTARTED
val address: String
def address: String
/**
* User overridable callback/setting.
@ -681,7 +682,9 @@ class LocalActorRef private[akka] (private[this] val actorFactory: () ⇒ Actor,
// ========= AKKA PROTECTED FUNCTIONS =========
@throws(classOf[java.io.ObjectStreamException])
private def writeReplace(): AnyRef = {
val inetaddr = Actor.remote.address
val inetaddr =
if (ReflectiveAccess.RemoteModule.isEnabled) Actor.remote.address
else ReflectiveAccess.RemoteModule.configDefaultAddress
SerializedActorRef(uuid, address, inetaddr.getAddress.getHostAddress, inetaddr.getPort, timeout)
}
@ -690,7 +693,7 @@ class LocalActorRef private[akka] (private[this] val actorFactory: () ⇒ Actor,
}
protected[akka] def postMessageToMailbox(message: Any, senderOption: Option[ActorRef]) {
dispatcher dispatchMessage new MessageInvocation(this, message, senderOption, None)
dispatcher dispatchMessage MessageInvocation(this, message, senderOption, None)
}
protected[akka] def postMessageToMailboxAndCreateFutureResultWithTimeout[T](
@ -699,8 +702,7 @@ class LocalActorRef private[akka] (private[this] val actorFactory: () ⇒ Actor,
senderOption: Option[ActorRef],
senderFuture: Option[Promise[T]]): Promise[T] = {
val future = if (senderFuture.isDefined) senderFuture else Some(new DefaultPromise[T](timeout))
dispatcher dispatchMessage new MessageInvocation(
this, message, senderOption, future.asInstanceOf[Some[Promise[Any]]])
dispatcher dispatchMessage MessageInvocation(this, message, senderOption, future.asInstanceOf[Some[Promise[Any]]])
future.get
}
@ -784,19 +786,12 @@ class LocalActorRef private[akka] (private[this] val actorFactory: () ⇒ Actor,
protected[akka] def restart(reason: Throwable, maxNrOfRetries: Option[Int], withinTimeRange: Option[Int]) {
def performRestart() {
val failedActor = actorInstance.get
failedActor match {
case p: Proxyable
failedActor.preRestart(reason)
failedActor.postRestart(reason)
case _
failedActor.preRestart(reason)
val freshActor = newActor
setActorSelfFields(failedActor, null) // Only null out the references if we could instantiate the new actor
actorInstance.set(freshActor) // Assign it here so if preStart fails, we can null out the sef-refs next call
freshActor.preStart()
freshActor.postRestart(reason)
}
failedActor.preRestart(reason)
val freshActor = newActor
setActorSelfFields(failedActor, null) // Only null out the references if we could instantiate the new actor
actorInstance.set(freshActor) // Assign it here so if preStart fails, we can null out the sef-refs next call
freshActor.preStart()
freshActor.postRestart(reason)
}
def tooManyRestarts() {
@ -865,20 +860,18 @@ class LocalActorRef private[akka] (private[this] val actorFactory: () ⇒ Actor,
private[this] def newActor: Actor = {
import Actor.{ actorRefInCreation refStack }
(try {
refStack.set(refStack.get.push(this))
val stackBefore = refStack.get
refStack.set(stackBefore.push(this))
try {
actorFactory()
} catch {
case e
val stack = refStack.get
//Clean up if failed
if ((stack.nonEmpty) && (stack.head eq this)) refStack.set(stack.pop)
//Then rethrow
throw e
}) match {
case null throw new ActorInitializationException("Actor instance passed to ActorRef can not be 'null'")
case valid valid
} finally {
val stackAfter = refStack.get
if (stackAfter.nonEmpty)
refStack.set(if (stackAfter.head eq null) stackAfter.pop.pop else stackAfter.pop) //pop null marker plus self
}
} match {
case null throw new ActorInitializationException("Actor instance passed to ActorRef can not be 'null'")
case valid valid
}
private def shutDownTemporaryActor(temporaryActor: ActorRef) {
@ -1268,6 +1261,10 @@ case class SerializedActorRef(val uuid: Uuid,
@throws(classOf[java.io.ObjectStreamException])
def readResolve(): AnyRef = Actor.registry.local.actorFor(uuid) match {
case Some(actor) actor
case None RemoteActorRef(new InetSocketAddress(hostname, port), address, timeout, None)
case None
if (ReflectiveAccess.RemoteModule.isEnabled)
RemoteActorRef(new InetSocketAddress(hostname, port), address, timeout, None)
else
throw new IllegalStateException("Trying to deserialize ActorRef (" + this + ") but it's not found in the local registry and remoting is not enabled!")
}
}

View file

@ -339,9 +339,7 @@ class Index[K <: AnyRef, V <: AnyRef: Manifest] {
*/
def foreach(fun: (K, V) Unit) {
import scala.collection.JavaConversions._
container.entrySet foreach { (e)
e.getValue.foreach(fun(e.getKey, _))
}
container.entrySet foreach { e e.getValue.foreach(fun(e.getKey, _)) }
}
/**

View file

@ -10,8 +10,8 @@ import akka.dispatch.{ MessageDispatcher, Dispatchers, Future }
import java.lang.reflect.{ InvocationTargetException, Method, InvocationHandler, Proxy }
import akka.util.{ Duration }
import java.util.concurrent.atomic.{ AtomicReference AtomVar }
import collection.immutable
//TODO Document this class, not only in Scaladoc, but also in a dedicated typed-actor.rst, for both java and scala
object TypedActor {
private val selfReference = new ThreadLocal[AnyRef]
@ -59,7 +59,7 @@ object TypedActor {
}
}
object Configuration {
object Configuration { //TODO: Replace this with the new ActorConfiguration when it exists
val defaultTimeout = Duration(Actor.TIMEOUT, "millis")
val defaultConfiguration = new Configuration(defaultTimeout, Dispatchers.defaultGlobalDispatcher)
def apply(): Configuration = defaultConfiguration
@ -84,6 +84,8 @@ object TypedActor {
}
case class SerializedMethodCall(ownerType: Class[_], methodName: String, parameterTypes: Array[Class[_]], parameterValues: Array[AnyRef]) {
//TODO implement writeObject and readObject to serialize
//TODO Possible optimization is to special encode the parameter-types to conserve space
private def readResolve(): AnyRef = MethodCall(ownerType.getDeclaredMethod(methodName, parameterTypes: _*), parameterValues)
}

View file

@ -129,13 +129,13 @@ object NodeAddress {
trait ClusterNode {
import ChangeListener._
val nodeAddress: NodeAddress
val zkServerAddresses: String
def nodeAddress: NodeAddress
def zkServerAddresses: String
val remoteClientLifeCycleListener: ActorRef
val remoteDaemon: ActorRef
val remoteService: RemoteSupport
val remoteServerAddress: InetSocketAddress
def remoteClientLifeCycleListener: ActorRef
def remoteDaemon: ActorRef
def remoteService: RemoteSupport
def remoteServerAddress: InetSocketAddress
val isConnected = new Switch(false)
val isLeader = new AtomicBoolean(false)

View file

@ -22,13 +22,8 @@ final case class MessageInvocation(receiver: ActorRef,
senderFuture: Option[Promise[Any]]) {
if (receiver eq null) throw new IllegalArgumentException("Receiver can't be null")
def invoke() {
try {
receiver.invoke(this)
} catch {
case e: NullPointerException throw new ActorInitializationException(
"Don't call 'self ! message' in the Actor's constructor (in Scala this means in the body of the class).")
}
final def invoke() {
receiver invoke this
}
}
@ -177,7 +172,7 @@ trait MessageDispatcher {
val uuid = i.next()
Actor.registry.local.actorFor(uuid) match {
case Some(actor) actor.stop()
case None {}
case None
}
}
}

View file

@ -104,7 +104,7 @@ object ReflectiveAccess {
object RemoteModule {
val TRANSPORT = Config.config.getString("akka.remote.layer", "akka.remote.netty.NettyRemoteSupport")
private[akka] val configDefaultAddress = new InetSocketAddress(Config.hostname, Config.remoteServerPort)
val configDefaultAddress = new InetSocketAddress(Config.hostname, Config.remoteServerPort)
lazy val isEnabled = remoteSupportClass.isDefined

View file

@ -10,43 +10,10 @@ HTTP
Module stability: **SOLID**
When using Akkas embedded servlet container
-------------------------------------------
Akka supports the JSR for REST called JAX-RS (JSR-311). It allows you to create interaction with your actors through HTTP + REST
You can deploy your REST services directly into the Akka kernel. All you have to do is to drop the JAR with your application containing the REST services into the $AKKA_HOME/deploy directory and specify in your akka.conf what resource packages to scan for (more on that below) and optionally define a “boot class” (if you need to create any actors or do any config). WAR deployment is coming soon.
Boot configuration class
------------------------
The boot class is needed for Akka to bootstrap the application and should contain the initial supervisor configuration of any actors in the module.
The boot class should be a regular POJO with a default constructor in which the initial configuration is done. The boot class then needs to be defined in the $AKKA_HOME/config/akka.conf config file like this:
.. code-block:: ruby
akka {
boot = ["sample.java.Boot", "sample.scala.Boot"] # FQN to the class doing initial actor
# supervisor bootstrap, should be defined in default constructor
...
}
After you've placed your service-jar into the $AKKA_HOME/deploy directory, you'll need to tell Akka where to look for your services, and you do that by specifying what packages you want Akka to scan for services, and that's done in akka.conf in the http-section:
.. code-block:: ruby
akka {
http {
...
resource-packages = ["com.bar","com.foo.bar"] # List with all resource packages for your Jersey services
...
}
When deploying in another servlet container:
When deploying in a servlet container:
--------------------------------------------
If you deploy Akka in another JEE container, don't forget to create an Akka initialization and cleanup hook:
If you deploy Akka in a JEE container, don't forget to create an Akka initialization and cleanup hook:
.. code-block:: scala
@ -86,32 +53,6 @@ Then you just declare it in your web.xml:
...
</web-app>
Also, you need to map the servlet that will handle your Jersey/JAX-RS calls, you use Jerseys ServletContainer servlet.
.. code-block:: xml
<web-app>
...
<servlet>
<servlet-name>Akka</servlet-name>
<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
<!-- And you want to configure your services -->
<init-param>
<param-name>com.sun.jersey.config.property.resourceConfigClass</param-name>
<param-value>com.sun.jersey.api.core.PackagesResourceConfig</param-value>
</init-param>
<init-param>
<param-name>com.sun.jersey.config.property.packages</param-name>
<param-value>your.resource.package.here;and.another.here;and.so.on</param-value>
</init-param>
</servlet>
<servlet-mapping>
<url-pattern>*</url-pattern>
<servlet-name>Akka</servlet-name>
</servlet-mapping>
...
</web-app>
Adapting your own Akka Initializer for the Servlet Container
------------------------------------------------------------
@ -142,16 +83,6 @@ If you want to use akka-camel or any other modules that have their own "Bootable
loader.boot(true, new BootableActorLoaderService with BootableRemoteActorService with CamelService) //<--- Important
}
Java API: Typed Actors
----------------------
`Sample module for REST services with Actors in Java <https://github.com/jboner/akka-modules/tree/v1.0/akka-samples/akka-sample-rest-java/src/main/java/sample/rest/java>`_
Scala API: Actors
-----------------
`Sample module for REST services with Actors in Scala <https://github.com/jboner/akka-modules/blob/v1.0/akka-samples/akka-sample-rest-scala/src/main/scala/SimpleService.scala>`_
Using Akka with the Pinky REST/MVC framework
--------------------------------------------
@ -193,6 +124,10 @@ In order to use Mist you have to register the MistServlet in *web.xml* or do the
<servlet>
<servlet-name>akkaMistServlet</servlet-name>
<servlet-class>akka.http.AkkaMistServlet</servlet-class>
<init-param> <!-- Optional, if empty or omitted, it will use the default in the akka.conf -->
<param-name>root-endpoint</param-name>
<param-value>address_of_root_endpoint_actor</param-value>
</init-param>
<!-- <async-supported>true</async-supported> Enable this for Servlet 3.0 support -->
</servlet>

View file

@ -1,266 +0,0 @@
HTTP Security
=============
.. sidebar:: Contents
.. contents:: :local:
Module stability: **IN PROGRESS**
Akka supports security for access to RESTful Actors through `HTTP Authentication <http://en.wikipedia.org/wiki/HTTP_Authentication>`_. The security is implemented as a jersey ResourceFilter which delegates the actual authentication to an authentication actor.
Akka provides authentication via the following authentication schemes:
* `Basic Authentication <http://en.wikipedia.org/wiki/Basic_access_authentication>`_
* `Digest Authentication <http://en.wikipedia.org/wiki/Digest_access_authentication>`_
* `Kerberos SPNEGO Authentication <http://en.wikipedia.org/wiki/SPNEGO>`_
The authentication is performed by implementations of akka.security.AuthenticationActor.
Akka provides a trait for each authentication scheme:
* BasicAuthenticationActor
* DigestAuthenticationActor
* SpnegoAuthenticationActor
Setup
-----
To secure your RESTful actors you need to perform the following steps:
1. configure the resource filter factory 'akka.security.AkkaSecurityFilterFactory' in the 'akka.conf' like this:
.. code-block:: ruby
akka {
...
rest {
filters="akka.security.AkkaSecurityFilterFactory"
}
...
}
2. Configure an implementation of an authentication actor in 'akka.conf':
.. code-block:: ruby
akka {
...
rest {
filters= ...
authenticator = "akka.security.samples.BasicAuthenticationService"
}
...
}
3. Start your authentication actor in your 'Boot' class. The security package consists of the following parts:
4. Secure your RESTful actors using class or resource level annotations:
* @DenyAll
* @RolesAllowed(listOfRoles)
* @PermitAll
Security Samples
----------------
The akka-samples-security module contains a small sample application with sample implementations for each authentication scheme.
You can start the sample app using the jetty plugin: mvn jetty:run.
The RESTful actor can then be accessed using your browser of choice under:
* permit access only to users having the “chef” role: `<http://localhost:8080//secureticker/chef>`_
* public access: `<http://localhost:8080//secureticker/public>`_
You can access the secured resource using any user for basic authentication (which is the default authenticator in the sample app).
Digest authentication can be directly enabled in the sample app. Kerberos/SPNEGO authentication is a bit more involved an is described below.
Kerberos/SPNEGO Authentication
------------------------------
Kerberos is a network authentication protocol, (see `<http://www.ietf.org/rfc/rfc1510.txt>`_). It provides strong authentication for client/server applications.
In a kerberos enabled environment a user will need to sign on only once. Subsequent authentication to applications is handled transparently by kerberos.
Most prominently the kerberos protocol is used to authenticate users in a windows network. When deploying web applications to a corporate intranet an important feature will be to support the single sign on (SSO), which comes to make the application kerberos aware.
How does it work (at least for REST actors)?
- When accessing a secured resource the server will check the request for the *Authorization* header as with basic or digest authentication.
- If it is not set, the server will respond with a challenge to "Negotiate". The negotiation is in fact the NEGO part of the `SPNEGO <http://tools.ietf.org/html/rfc4178>`_ specification
- The browser will then try to acquire a so called *service ticket* from a ticket granting service, i.e. the kerberos server
- The browser will send the *service ticket* to the web application encoded in the header value of the *Authorization* header
- The web application must validate the ticket based on a shared secret between the web application and the kerberos server. As a result the web application will know the name of the user
To activate the kerberos/SPNEGO authentication for your REST actor you need to enable the kerberos/SPNEGOauthentication actor in the akka.conf like this:
.. code-block:: ruby
akka {
...
rest {
filters= ...
authenticator = "akka.security.samples.SpnegoAuthenticationService"
}
...
}
Furthermore you must provide the SpnegoAuthenticator with the following information.
- Service principal name: the name of your web application in the kerberos servers user database. This name is always has the form ``HTTP/{server}@{realm}``
- Path to the keytab file: this is a kind of certificate for your web application to acquire tickets from the kerberos server
.. code-block:: ruby
akka {
...
rest {
filters= ...
authenticator = "akka.security.samples.SpnegoAuthenticationService"
kerberos {
servicePrincipal = "HTTP/{server}@{realm}"
keyTabLocation = "URL to keytab"
# kerberosDebug = "true"
}
}
...
}
How to setup kerberos on localhost for Ubuntu
---------------------------------------------
This is a short step by step description of howto set up a kerberos server on an ubuntu system.
1. Install the Heimdal Kerberos Server and Client
::
sudo apt-get install heimdal-clients heimdal-clients-x heimdal-kdc krb5-config
...
2. Set up your kerberos realm. In this example the realm is of course … EXAMPLE.COM
::
eckart@dilbert:~$ sudo kadmin -l
kadmin> init EXAMPLE.COM
Realm max ticket life [unlimited]:
Realm max renewable ticket life [unlimited]:
kadmin> quit
3. Tell your kerberos clients what your realm is and where to find the kerberos server (aka the Key Distribution Centre or KDC)
Edit the kerberos config file: /etc/krb5.conf and configure …
…the default realm:
::
[libdefaults]
default_realm = EXAMPLE.COM
… where to find the KDC for your realm
::
[realms]
EXAMPLE.COM = {
kdc = localhost
}
…which hostnames or domains map to which realm (a kerberos realm is **not** a DNS domain):
::
[domain_realm]
localhost = EXAMPLE.COM
4. Add the principals
The user principal:
::
eckart@dilbert:~$ sudo kadmin -l
kadmin> add zaphod
Max ticket life [1 day]:
Max renewable life [1 week]:
Principal expiration time [never]:
Password expiration time [never]:
Attributes []:
zaphod@EXAMPLE.COM's Password:
Verifying - zaphod@EXAMPLE.COM's Password:
kadmin> quit
The service principal:
::
eckart@dilbert:~$ sudo kadmin -l
kadmin> add HTTP/localhost@EXAMPLE.COM
Max ticket life [1 day]:
Max renewable life [1 week]:
Principal expiration time [never]:
Password expiration time [never]:
Attributes []:
HTTP/localhost@EXAMPLE.COM's Password:
Verifying - HTTP/localhost@EXAMPLE.COM's Password:
kadmin> quit
We can now try to acquire initial tickets for the principals to see if everything worked.
::
eckart@dilbert:~$ kinit zaphod
zaphod@EXAMPLE.COM's Password:
If this method returns withour error we have a success.
We can additionally list the acquired tickets:
::
eckart@dilbert:~$ klist
Credentials cache: FILE:/tmp/krb5cc_1000
Principal: zaphod@EXAMPLE.COM
Issued Expires Principal
Oct 24 21:51:59 Oct 25 06:51:59 krbtgt/EXAMPLE.COM@EXAMPLE.COM
This seems correct. To remove the ticket cache simply type kdestroy.
5. Create a keytab for your service principal
::
eckart@dilbert:~$ ktutil -k http.keytab add -p HTTP/localhost@EXAMPLE.COM -V 1 -e aes256-cts-hmac-sha1-96
Password:
Verifying - Password:
eckart@dilbert:~$
This command will create a keytab file for the service principal named ``http.keytab`` in the current directory. You can specify other encryption methods than aes256-cts-hmac-sha1-96, but this is the e default encryption method for the heimdal client, so there is no additional configuration needed. You can specify other encryption types in the krb5.conf.
Note that you might need to install the unlimited strength policy files for java from here: `<http://java.sun.com/javase/downloads/index_jdk5.jsp>`_ to use the aes256 encryption from your application.
Again we can test if the keytab generation worked with the kinit command:
::
eckart@dilbert:~$ kinit -t http.keytab HTTP/localhost@EXAMPLE.COM
eckart@dilbert:~$ klist
Credentials cache: FILE:/tmp/krb5cc_1000
Principal: HTTP/localhost@EXAMPLE.COM
Issued Expires Principal
Oct 24 21:59:20 Oct 25 06:59:20 krbtgt/EXAMPLE.COM@EXAMPLE.COM
Now point the configuration of the key in 'akka.conf' to the correct location and set the correct service principal name. The web application should now startup and produce at least a 401 response with a header ``WWW-Authenticate`` = "Negotiate". The last step is to configure the browser.
6. Set up Firefox to use Kerberos/SPNEGO
This is done by typing ``about:config``. Filter the config entries for ``network.neg`` and set the config entries ``network.negotiate-auth.delegation-uris`` and ``network.negotiate-auth.trusted-uris`` to ``localhost``.
and now …
7. Access the RESTful Actor.
8. Have fun
… but acquire an initial ticket for the user principal first: ``kinit zaphod``

View file

@ -1,38 +0,0 @@
/**
* Copyright (C) 2009-2011 Scalable Solutions AB <http://scalablesolutions.se>
*/
package akka.http
import com.sun.jersey.spi.container.servlet.ServletContainer
/**
* This is just a simple wrapper on top of ServletContainer to inject some config from the akka.conf
* If you were using akka.comet.AkkaServlet before, but only use it for Jersey, you should switch to this servlet instead
*/
class AkkaRestServlet extends ServletContainer {
import akka.config.Config.{ config c }
val initParams = new java.util.HashMap[String, String]
addInitParameter("com.sun.jersey.config.property.packages", c.getList("akka.http.resource-packages").mkString(";"))
addInitParameter("com.sun.jersey.spi.container.ResourceFilters", c.getList("akka.http.filters").mkString(","))
/**
* Provide a fallback for default values
*/
override def getInitParameter(key: String) =
Option(super.getInitParameter(key)).getOrElse(initParams get key)
/**
* Provide a fallback for default values
*/
override def getInitParameterNames() = {
import scala.collection.JavaConversions._
initParams.keySet.iterator ++ super.getInitParameterNames
}
/**
* Provide possibility to add config params
*/
def addInitParameter(param: String, value: String): Unit = initParams.put(param, value)
}

View file

@ -11,6 +11,7 @@ import akka.config.ConfigurationException
import javax.servlet.http.{ HttpServletResponse, HttpServletRequest }
import javax.servlet.http.HttpServlet
import javax.servlet.Filter
import java.lang.UnsupportedOperationException
/**
* @author Garrick Evans
@ -71,27 +72,39 @@ trait Mist {
/**
* The root endpoint actor
*/
protected val _root = Actor.registry.actorFor(RootActorID).getOrElse(
throw new ConfigurationException("akka.http.root-actor-id configuration option does not have a valid actor address [" + RootActorID + "]"))
def root: ActorRef
/**
* Server-specific method factory
*/
protected var _factory: Option[RequestMethodFactory] = None
protected var factory: Option[RequestMethodFactory] = None
/**
* Handles all servlet requests
*/
protected def mistify(request: HttpServletRequest,
response: HttpServletResponse)(builder: (() tAsyncRequestContext) RequestMethod) = {
def suspend: tAsyncRequestContext = {
response: HttpServletResponse) = {
val builder: (() tAsyncRequestContext) RequestMethod =
request.getMethod.toUpperCase match {
case "DELETE" factory.get.Delete
case "GET" factory.get.Get
case "HEAD" factory.get.Head
case "OPTIONS" factory.get.Options
case "POST" factory.get.Post
case "PUT" factory.get.Put
case "TRACE" factory.get.Trace
case unknown throw new UnsupportedOperationException(unknown)
}
def suspend(closeConnection: Boolean): tAsyncRequestContext = {
// set to right now, which is effectively "already expired"
response.setDateHeader("Expires", System.currentTimeMillis)
response.setHeader("Cache-Control", "no-cache, must-revalidate")
// no keep-alive?
if (ConnectionClose) response.setHeader("Connection", "close")
if (closeConnection) response.setHeader("Connection", "close")
// suspend the request
// TODO: move this out to the specialized support if jetty asyncstart doesnt let us update TOs
@ -100,8 +113,8 @@ trait Mist {
// shoot the message to the root endpoint for processing
// IMPORTANT: the suspend method is invoked on the server thread not in the actor
val method = builder(suspend _)
if (method.go) _root ! method
val method = builder(() suspend(ConnectionClose))
if (method.go) root ! method
}
/**
@ -111,7 +124,7 @@ trait Mist {
def initMist(context: ServletContext) {
val server = context.getServerInfo
val (major, minor) = (context.getMajorVersion, context.getMinorVersion)
_factory = if (major >= 3) {
factory = if (major >= 3) {
Some(Servlet30ContextMethodFactory)
} else if (server.toLowerCase startsWith JettyServer) {
Some(JettyContinuationMethodFactory)
@ -121,11 +134,23 @@ trait Mist {
}
}
trait RootEndpointLocator {
var root: ActorRef = null
def configureRoot(address: String) {
def findRoot(address: String): ActorRef =
Actor.registry.actorFor(address).getOrElse(
throw new ConfigurationException("akka.http.root-actor-id configuration option does not have a valid actor address [" + address + "]"))
root = if ((address eq null) || address == "") findRoot(MistSettings.RootActorID) else findRoot(address)
}
}
/**
* AkkaMistServlet adds support to bridge Http and Actors in an asynchronous fashion
* Async impls currently supported: Servlet3.0, Jetty Continuations
*/
class AkkaMistServlet extends HttpServlet with Mist {
class AkkaMistServlet extends HttpServlet with Mist with RootEndpointLocator {
import javax.servlet.{ ServletConfig }
/**
@ -134,22 +159,17 @@ class AkkaMistServlet extends HttpServlet with Mist {
override def init(config: ServletConfig) {
super.init(config)
initMist(config.getServletContext)
configureRoot(config.getServletContext.getInitParameter("root-endpoint"))
}
protected override def doDelete(req: HttpServletRequest, res: HttpServletResponse) = mistify(req, res)(_factory.get.Delete)
protected override def doGet(req: HttpServletRequest, res: HttpServletResponse) = mistify(req, res)(_factory.get.Get)
protected override def doHead(req: HttpServletRequest, res: HttpServletResponse) = mistify(req, res)(_factory.get.Head)
protected override def doOptions(req: HttpServletRequest, res: HttpServletResponse) = mistify(req, res)(_factory.get.Options)
protected override def doPost(req: HttpServletRequest, res: HttpServletResponse) = mistify(req, res)(_factory.get.Post)
protected override def doPut(req: HttpServletRequest, res: HttpServletResponse) = mistify(req, res)(_factory.get.Put)
protected override def doTrace(req: HttpServletRequest, res: HttpServletResponse) = mistify(req, res)(_factory.get.Trace)
protected override def service(req: HttpServletRequest, res: HttpServletResponse) = mistify(req, res)
}
/**
* Proof-of-concept, use at own risk
* Will be officially supported in a later release
*/
class AkkaMistFilter extends Filter with Mist {
class AkkaMistFilter extends Filter with Mist with RootEndpointLocator {
import javax.servlet.{ ServletRequest, ServletResponse, FilterConfig, FilterChain }
/**
@ -157,6 +177,7 @@ class AkkaMistFilter extends Filter with Mist {
*/
def init(config: FilterConfig) {
initMist(config.getServletContext)
configureRoot(config.getServletContext.getInitParameter("root-endpoint"))
}
/**
@ -165,16 +186,7 @@ class AkkaMistFilter extends Filter with Mist {
override def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain) {
(req, res) match {
case (hreq: HttpServletRequest, hres: HttpServletResponse)
hreq.getMethod.toUpperCase match {
case "DELETE" mistify(hreq, hres)(_factory.get.Delete)
case "GET" mistify(hreq, hres)(_factory.get.Get)
case "HEAD" mistify(hreq, hres)(_factory.get.Head)
case "OPTIONS" mistify(hreq, hres)(_factory.get.Options)
case "POST" mistify(hreq, hres)(_factory.get.Post)
case "PUT" mistify(hreq, hres)(_factory.get.Put)
case "TRACE" mistify(hreq, hres)(_factory.get.Trace)
case unknown {}
}
mistify(hreq, hres)
chain.doFilter(req, res)
case _ chain.doFilter(req, res)
}
@ -276,7 +288,7 @@ class RootEndpoint extends Actor with Endpoint {
def recv: Receive = {
case NoneAvailable(uri, req) _na(uri, req)
case unknown {}
case unknown
}
/**
@ -329,24 +341,22 @@ trait RequestMethod {
def request = context.get.getRequest.asInstanceOf[HttpServletRequest]
def response = context.get.getResponse.asInstanceOf[HttpServletResponse]
def getHeaderOrElse(name: String, default: Function[Any, String]): String =
def getHeaderOrElse(name: String, default: String): String =
request.getHeader(name) match {
case null default(null)
case null default
case s s
}
def getParameterOrElse(name: String, default: Function[Any, String]): String =
def getParameterOrElse(name: String, default: String): String =
request.getParameter(name) match {
case null default(null)
case null default
case s s
}
def complete(status: Int, body: String): Boolean = complete(status, body, Headers())
def complete(status: Int, body: String, headers: Headers): Boolean =
def complete(status: Int, body: String, headers: Headers = Headers()): Boolean =
rawComplete { res
res.setStatus(status)
headers foreach { h response.setHeader(h._1, h._2) }
headers foreach { case (name, value) response.setHeader(name, value) }
res.getWriter.write(body)
res.getWriter.close
res.flushBuffer

View file

@ -45,8 +45,8 @@ trait Servlet30Context extends AsyncListener {
//
def onComplete(e: AsyncEvent) {}
def onError(e: AsyncEvent) = e.getThrowable match {
case null {}
case t {}
case null
case t EventHandler.error(t, this, t.getMessage)
}
def onStartAsync(e: AsyncEvent) {}
def onTimeout(e: AsyncEvent) = {

View file

@ -1,563 +0,0 @@
/*
* 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 akka.security
import akka.actor.{ Scheduler, Actor, ActorRef, IllegalActorStateException }
import akka.event.EventHandler
import akka.actor.Actor._
import akka.config.{ Config, ConfigurationException }
import com.sun.jersey.api.model.AbstractMethod
import com.sun.jersey.spi.container.{ ResourceFilterFactory, ContainerRequest, ContainerRequestFilter, ContainerResponse, ContainerResponseFilter, ResourceFilter }
import com.sun.jersey.core.util.Base64
import javax.ws.rs.core.{ SecurityContext, Context, Response }
import javax.ws.rs.WebApplicationException
import javax.annotation.security.{ DenyAll, PermitAll, RolesAllowed }
import java.security.Principal
import java.util.concurrent.TimeUnit
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
case class SpnegoCredentials(token: Array[Byte]) extends Credentials
/**
* Jersey Filter for invocation intercept and authorization/authentication
*/
class AkkaSecurityFilterFactory extends ResourceFilterFactory {
class Filter(actor: ActorRef, rolesAllowed: Option[List[String]])
extends ResourceFilter with ContainerRequestFilter {
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 = (authenticator !! Authenticate(request, roles)).as[AnyRef]
result match {
case Some(OK) request
case Some(r) if r.isInstanceOf[Response]
throw new WebApplicationException(r.asInstanceOf[Response])
case None throw new WebApplicationException(408)
case unknown {
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR)
}
}
}
case None throw new WebApplicationException(Response.Status.FORBIDDEN)
}
}
lazy val authenticatorFQN = {
val auth = Config.config.getString("akka.http.authenticator", "N/A")
if (auth == "N/A") throw new IllegalActorStateException("The config option 'akka.http.authenticator' is not defined in 'akka.conf'")
auth
}
/**
* 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: ActorRef = Actor.registry.actorFor(authenticatorFQN)
.getOrElse(throw new ConfigurationException(
"akka.http.authenticator configuration option does not have a valid actor address [" + authenticatorFQN + "]"))
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(cra.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 {
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 security context is created for the ContainerRequest
* this should ensure good integration with current Jersey security
*/
protected val authenticate: Receive = {
case Authenticate(req, roles) {
verify(extractCredentials(req)) match {
case Some(u: UserInfo) {
req.setSecurityContext(mkSecurityContext(req, u))
if (roles.exists(req.isUserInRole(_))) self.reply(OK)
else self.reply(Response.status(Response.Status.FORBIDDEN).build)
}
case _ self.reply(unauthorized)
}
}
}
def receive = 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 ne 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 Authorization = """(.*):(.*)""".r
authOption(r) match {
case Some(token) {
val authResponse = new String(Base64.decode(token.substring(6).getBytes))
authResponse match {
case Authorization(username, password) Some(BasicCredentials(username, password))
case _ None
}
}
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 LiftUtils._
private object InvalidateNonces
//Holds the generated nonces for the specified validity period
val nonceMap = mkNonceMap
//Discards old nonces
protected val invalidateNonces: Receive = {
case InvalidateNonces
val ts = System.currentTimeMillis
nonceMap.filter(tuple (ts - tuple._2) < nonceValidityPeriod)
case unknown {}
}
//Schedule the invalidation of nonces
Scheduler.schedule(self, InvalidateNonces, noncePurgeInterval, noncePurgeInterval, TimeUnit.MILLISECONDS)
//authenticate or invalidate nonces
override def receive = 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
}
import java.security.Principal
import java.security.PrivilegedActionException
import java.security.PrivilegedExceptionAction
import javax.security.auth.login.AppConfigurationEntry
import javax.security.auth.login.Configuration
import javax.security.auth.login.LoginContext
import javax.security.auth.Subject
import javax.security.auth.kerberos.KerberosPrincipal
import org.ietf.jgss.GSSContext
import org.ietf.jgss.GSSCredential
import org.ietf.jgss.GSSManager
trait SpnegoAuthenticationActor extends AuthenticationActor[SpnegoCredentials] {
override def unauthorized =
Response.status(401).header("WWW-Authenticate", "Negotiate").build
// for some reason the jersey Base64 class does not work with kerberos
// but the commons Base64 does
import org.apache.commons.codec.binary.Base64
override def extractCredentials(r: Req): Option[SpnegoCredentials] = {
val AuthHeader = """Negotiate\s(.*)""".r
authOption(r) match {
case Some(AuthHeader(token))
Some(SpnegoCredentials(Base64.decodeBase64(token.trim.getBytes)))
case _ None
}
}
override def verify(odc: Option[SpnegoCredentials]): Option[UserInfo] = odc match {
case Some(dc) {
try {
val principal = Subject.doAs(this.serviceSubject, new KerberosValidateAction(dc.token));
val user = stripRealmFrom(principal)
Some(UserInfo(user, null, rolesFor(user)))
} catch {
case e: PrivilegedActionException {
EventHandler.error(e, this, e.getMessage)
None
}
}
}
case _ None
}
override def mkSecurityContext(r: Req, u: UserInfo): SecurityContext =
mkDefaultSecurityContext(r, u, SecurityContext.CLIENT_CERT_AUTH) // the security context does not know about spnego/kerberos
// not sure whether to use a constant from the security context or something like "SPNEGO/Kerberos"
/**
* returns the roles for the given user
*/
def rolesFor(user: String): List[String]
// Kerberos
/**
* strips the realm from a kerberos principal name, returning only the user part
*/
private def stripRealmFrom(principal: String): String = principal.split("@")(0)
/**
* principal name for the HTTP kerberos service, i.e HTTP/ { server } @ { realm }
*/
lazy val servicePrincipal = {
val p = Config.config.getString("akka.http.kerberos.servicePrincipal", "N/A")
if (p == "N/A") throw new IllegalActorStateException("The config option 'akka.http.kerberos.servicePrincipal' is not defined in 'akka.conf'")
p
}
/**
* keytab location with credentials for the service principal
*/
lazy val keyTabLocation = {
val p = Config.config.getString("akka.http.kerberos.keyTabLocation", "N/A")
if (p == "N/A") throw new IllegalActorStateException("The config option 'akka.http.kerberos.keyTabLocation' is not defined in 'akka.conf'")
p
}
lazy val kerberosDebug = {
val p = Config.config.getString("akka.http.kerberos.kerberosDebug", "N/A")
if (p == "N/A") throw new IllegalActorStateException("The config option 'akka.http.kerberos.kerberosDebug' is not defined in 'akka.conf'")
p
}
/**
* is not used by this authenticator, so accept an empty value
*/
lazy val realm = Config.config.getString("akka.http.kerberos.realm", "")
/**
* verify the kerberos token from a client with the server
*/
class KerberosValidateAction(kerberosTicket: Array[Byte]) extends PrivilegedExceptionAction[String] {
def run = {
val context = GSSManager.getInstance().createContext(null.asInstanceOf[GSSCredential])
context.acceptSecContext(kerberosTicket, 0, kerberosTicket.length)
val user = context.getSrcName().toString()
context.dispose()
user
}
}
// service principal login to kerberos on startup
val serviceSubject = servicePrincipalLogin
/**
* acquire an initial ticket from the kerberos server for the HTTP service
*/
def servicePrincipalLogin = {
val loginConfig = new LoginConfig(
new java.net.URL(this.keyTabLocation).toExternalForm(),
this.servicePrincipal,
this.kerberosDebug)
val princ = new java.util.HashSet[Principal](1)
princ.add(new KerberosPrincipal(this.servicePrincipal))
val sub = new Subject(false, princ, new java.util.HashSet[Object], new java.util.HashSet[Object])
val lc = new LoginContext("", sub, null, loginConfig)
lc.login()
lc.getSubject()
}
/**
* this class simulates a login-config.xml
*/
class LoginConfig(keyTabLocation: String, servicePrincipal: String, debug: String) extends Configuration {
override def getAppConfigurationEntry(name: String): Array[AppConfigurationEntry] = {
val options = new java.util.HashMap[String, String]
options.put("useKeyTab", "true")
options.put("keyTab", this.keyTabLocation)
options.put("principal", this.servicePrincipal)
options.put("storeKey", "true")
options.put("doNotPrompt", "true")
options.put("isInitiator", "true")
options.put("debug", debug)
Array(new AppConfigurationEntry(
"com.sun.security.auth.module.Krb5LoginModule",
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
options))
}
}
}
/*
* Copyright 2006-2010 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.
*/
object LiftUtils {
import java.security.{ MessageDigest, SecureRandom }
val random = new SecureRandom()
def md5(in: Array[Byte]): Array[Byte] = (MessageDigest.getInstance("MD5")).digest(in)
/**
* Create a random string of a given size
* @param size size of the string to create. Must be a positive or nul integer
* @return the generated string
*/
def randomString(size: Int): String = {
def addChar(pos: Int, lastRand: Int, sb: StringBuilder): StringBuilder = {
if (pos >= size) sb
else {
val randNum = if ((pos % 6) == 0) random.nextInt else lastRand
sb.append((randNum & 0x1f) match {
case n if n < 26 ('A' + n).toChar
case n ('0' + (n - 26)).toChar
})
addChar(pos + 1, randNum >> 5, sb)
}
}
addChar(0, 0, new StringBuilder(size)).toString
}
/** encode a Byte array as hexadecimal characters */
def hexEncode(in: Array[Byte]): String = {
val sb = new StringBuilder
val len = in.length
def addDigit(in: Array[Byte], pos: Int, len: Int, sb: StringBuilder) {
if (pos < len) {
val b: Int = in(pos)
val msb = (b & 0xf0) >> 4
val lsb = (b & 0x0f)
sb.append((if (msb < 10) ('0' + msb).asInstanceOf[Char] else ('a' + (msb - 10)).asInstanceOf[Char]))
sb.append((if (lsb < 10) ('0' + lsb).asInstanceOf[Char] else ('a' + (lsb - 10)).asInstanceOf[Char]))
addDigit(in, pos + 1, len, sb)
}
}
addDigit(in, 0, len, sb)
sb.toString
}
/**
* Splits a string of the form &lt;name1=value1, name2=value2, ... &gt; and unquotes the quoted values.
* The result is a Map[String, String]
*/
def splitNameValuePairs(props: String): Map[String, String] = {
/**
* If str is surrounded by quotes it return the content between the quotes
*/
def unquote(str: String) = {
if ((str ne null) && str.length >= 2 && str.charAt(0) == '\"' && str.charAt(str.length - 1) == '\"')
str.substring(1, str.length - 1)
else
str
}
val list = props.split(",").toList.map(in {
val pair = in match { case null Nil case s s.split("=").toList.map(_.trim).filter(_.length > 0) }
(pair(0), unquote(pair(1)))
})
val map: Map[String, String] = Map.empty
(map /: list)((m, next) m + (next))
}
}

View file

@ -1,15 +0,0 @@
package akka.security
import junit.framework.Test
import junit.framework.TestCase
import junit.framework.TestSuite
object AllTest extends TestCase {
def suite(): Test = {
val suite = new TestSuite("All Scala tests")
suite.addTestSuite(classOf[BasicAuthenticatorSpec])
suite
}
def main(args: Array[String]) = junit.textui.TestRunner.run(suite)
}

View file

@ -1,82 +0,0 @@
/**
* Copyright (C) 2009-2011 Scalable Solutions AB <http://scalablesolutions.se>
*/
package akka.security
import akka.config.Supervision._
import akka.actor.Actor._
import org.scalatest.Suite
import org.scalatest.junit.JUnitSuite
import org.scalatest.matchers.MustMatchers
import org.scalatest.mock.MockitoSugar
import org.mockito.Mockito._
import org.mockito.Matchers._
import org.junit.{ Before, After, Test }
import javax.ws.rs.core.{ SecurityContext, Context, Response }
import com.sun.jersey.spi.container.{ ResourceFilterFactory, ContainerRequest, ContainerRequestFilter, ContainerResponse, ContainerResponseFilter, ResourceFilter }
import com.sun.jersey.core.util.Base64
object BasicAuthenticatorSpec {
class BasicAuthenticator extends BasicAuthenticationActor {
def verify(odc: Option[BasicCredentials]): Option[UserInfo] = odc match {
case Some(dc) Some(UserInfo("foo", "bar", "ninja" :: "chef" :: Nil))
case _ None
}
override def realm = "test"
}
}
class BasicAuthenticatorSpec extends junit.framework.TestCase
with Suite with MockitoSugar with MustMatchers {
import BasicAuthenticatorSpec._
val authenticator = actorOf[BasicAuthenticator]
authenticator.start()
@Test
def testChallenge = {
val req = mock[ContainerRequest]
val result = (authenticator !! (Authenticate(req, List("foo")), 10000)).as[Response].get
// the actor replies with a challenge for the browser
result.getStatus must equal(Response.Status.UNAUTHORIZED.getStatusCode)
result.getMetadata.get("WWW-Authenticate").get(0).toString must startWith("Basic")
}
@Test
def testAuthenticationSuccess = {
val req = mock[ContainerRequest]
// fake a basic auth header -> this will authenticate the user
when(req.getHeaderValue("Authorization")).thenReturn("Basic " + new String(Base64.encode("foo:bar")))
// fake a request authorization -> this will authorize the user
when(req.isUserInRole("chef")).thenReturn(true)
val result = (authenticator !! (Authenticate(req, List("chef")), 10000)).as[AnyRef].get
result must be(OK)
// the authenticator must have set a security context
verify(req).setSecurityContext(any[SecurityContext])
}
@Test
def testUnauthorized = {
val req = mock[ContainerRequest]
// fake a basic auth header -> this will authenticate the user
when(req.getHeaderValue("Authorization")).thenReturn("Basic " + new String(Base64.encode("foo:bar")))
when(req.isUserInRole("chef")).thenReturn(false) // this will deny access
val result = (authenticator !! (Authenticate(req, List("chef")), 10000)).as[Response].get
result.getStatus must equal(Response.Status.FORBIDDEN.getStatusCode)
// the authenticator must have set a security context
verify(req).setSecurityContext(any[SecurityContext])
}
}

View file

@ -16,17 +16,10 @@ class ConfigSpec extends WordSpec with MustMatchers {
"contain all configuration properties for akka-http that are used in code with their correct defaults" in {
import Config.config._
getString("akka.http.authenticator") must equal(Some("N/A"))
getBool("akka.http.connection-close") must equal(Some(true))
getString("akka.http.expired-header-name") must equal(Some("Async-Timeout"))
getList("akka.http.filters") must equal(List("akka.security.AkkaSecurityFilterFactory"))
getList("akka.http.resource-packages") must equal(Nil)
getString("akka.http.hostname") must equal(Some("localhost"))
getString("akka.http.expired-header-value") must equal(Some("expired"))
getString("akka.http.kerberos.servicePrincipal") must equal(Some("N/A"))
getString("akka.http.kerberos.keyTabLocation") must equal(Some("N/A"))
getString("akka.http.kerberos.kerberosDebug") must equal(Some("N/A"))
getString("akka.http.kerberos.realm") must equal(Some(""))
getInt("akka.http.port") must equal(Some(9998))
getBool("akka.http.root-actor-builtin") must equal(Some(true))
getString("akka.http.root-actor-id") must equal(Some("_httproot"))

View file

@ -204,31 +204,6 @@ akka {
hostname = "localhost"
port = 9998
#If you are using akka.http.AkkaRestServlet
filters = ["akka.security.AkkaSecurityFilterFactory"] # List with all jersey filters to use
# resource-packages = ["sample.rest.scala",
# "sample.rest.java",
# "sample.security"] # List with all resource packages for your Jersey services
resource-packages = []
# The authentication service to use. Need to be overridden (sample now)
# authenticator = "sample.security.BasicAuthenticationService"
authenticator = "N/A"
# Uncomment if you are using the KerberosAuthenticationActor
# kerberos {
# servicePrincipal = "HTTP/localhost@EXAMPLE.COM"
# keyTabLocation = "URL to keytab"
# kerberosDebug = "true"
# realm = "EXAMPLE.COM"
# }
kerberos {
servicePrincipal = "N/A"
keyTabLocation = "N/A"
kerberosDebug = "N/A"
realm = ""
}
# If you are using akka.http.AkkaMistServlet
mist-dispatcher {
#type = "GlobalDispatcher" # Uncomment if you want to use a different dispatcher than the default one for Comet