changed trapExit from Boolean to "trapExit = List(classOf[..], classOf[..])" + cleaned up security code
This commit is contained in:
parent
495adb7898
commit
d7ac449d26
7 changed files with 465 additions and 371 deletions
132
.idea/workspace.xml
generated
132
.idea/workspace.xml
generated
|
|
@ -2,10 +2,13 @@
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ChangeListManager" verified="true">
|
<component name="ChangeListManager" verified="true">
|
||||||
<list default="true" readonly="true" id="e5228cd0-f5f0-4bab-b96d-de1a99e71911" name="Default" comment="">
|
<list default="true" readonly="true" id="e5228cd0-f5f0-4bab-b96d-de1a99e71911" name="Default" comment="">
|
||||||
<change type="MODIFICATION" beforePath="$PROJECT_DIR$/pom.xml" afterPath="$PROJECT_DIR$/pom.xml" />
|
<change type="MODIFICATION" beforePath="$PROJECT_DIR$/akka-actors/src/main/scala/actor/Actor.scala" afterPath="$PROJECT_DIR$/akka-actors/src/main/scala/actor/Actor.scala" />
|
||||||
<change type="MODIFICATION" beforePath="$PROJECT_DIR$/akka.iml" afterPath="$PROJECT_DIR$/akka.iml" />
|
<change type="MODIFICATION" beforePath="$PROJECT_DIR$/akka-security/src/main/scala/Security.scala" afterPath="$PROJECT_DIR$/akka-security/src/main/scala/Security.scala" />
|
||||||
<change type="DELETED" beforePath="$PROJECT_DIR$/akka.ipr" afterPath="" />
|
<change type="MODIFICATION" beforePath="$PROJECT_DIR$/akka-actors/src/main/scala/actor/Supervisor.scala" afterPath="$PROJECT_DIR$/akka-actors/src/main/scala/actor/Supervisor.scala" />
|
||||||
<change type="DELETED" beforePath="$PROJECT_DIR$/akka.iws" afterPath="" />
|
<change type="MODIFICATION" beforePath="$PROJECT_DIR$/.idea/workspace.xml" afterPath="$PROJECT_DIR$/.idea/workspace.xml" />
|
||||||
|
<change type="MODIFICATION" beforePath="$PROJECT_DIR$/akka-security/src/test/scala/SecuritySpec.scala" afterPath="$PROJECT_DIR$/akka-security/src/test/scala/SecuritySpec.scala" />
|
||||||
|
<change type="MODIFICATION" beforePath="$PROJECT_DIR$/akka-amqp/src/main/scala/AMQP.scala" afterPath="$PROJECT_DIR$/akka-amqp/src/main/scala/AMQP.scala" />
|
||||||
|
<change type="MODIFICATION" beforePath="$PROJECT_DIR$/akka-actors/src/main/scala/actor/Scheduler.scala" afterPath="$PROJECT_DIR$/akka-actors/src/main/scala/actor/Scheduler.scala" />
|
||||||
</list>
|
</list>
|
||||||
<ignored path=".idea/workspace.xml" />
|
<ignored path=".idea/workspace.xml" />
|
||||||
<ignored path="akka.iws" />
|
<ignored path="akka.iws" />
|
||||||
|
|
@ -65,10 +68,64 @@
|
||||||
<component name="FileColors" enabled="true" enabledForTabs="true" />
|
<component name="FileColors" enabled="true" enabledForTabs="true" />
|
||||||
<component name="FileEditorManager">
|
<component name="FileEditorManager">
|
||||||
<leaf>
|
<leaf>
|
||||||
<file leaf-file-name="Actor.scala" pinned="false" current="true" current-in-tab="true">
|
<file leaf-file-name="Actor.scala" pinned="false" current="false" current-in-tab="false">
|
||||||
<entry file="file://$PROJECT_DIR$/akka-actors/src/main/scala/actor/Actor.scala">
|
<entry file="file://$PROJECT_DIR$/akka-actors/src/main/scala/actor/Actor.scala">
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
<provider selected="true" editor-type-id="text-editor">
|
||||||
<state line="162" column="71" selection-start="6288" selection-end="6288" vertical-scroll-proportion="1.7435898">
|
<state line="75" column="6" selection-start="2861" selection-end="2861" vertical-scroll-proportion="0.0">
|
||||||
|
<folding />
|
||||||
|
</state>
|
||||||
|
</provider>
|
||||||
|
</entry>
|
||||||
|
</file>
|
||||||
|
<file leaf-file-name="Scheduler.scala" pinned="false" current="false" current-in-tab="false">
|
||||||
|
<entry file="file://$PROJECT_DIR$/akka-actors/src/main/scala/actor/Scheduler.scala">
|
||||||
|
<provider selected="true" editor-type-id="text-editor">
|
||||||
|
<state line="45" column="0" selection-start="1590" selection-end="1628" vertical-scroll-proportion="0.0">
|
||||||
|
<folding />
|
||||||
|
</state>
|
||||||
|
</provider>
|
||||||
|
</entry>
|
||||||
|
</file>
|
||||||
|
<file leaf-file-name="Config.scala" pinned="false" current="false" current-in-tab="false">
|
||||||
|
<entry file="file://$PROJECT_DIR$/akka-actors/src/main/scala/config/Config.scala">
|
||||||
|
<provider selected="true" editor-type-id="text-editor">
|
||||||
|
<state line="22" column="13" selection-start="649" selection-end="649" vertical-scroll-proportion="0.0">
|
||||||
|
<folding />
|
||||||
|
</state>
|
||||||
|
</provider>
|
||||||
|
</entry>
|
||||||
|
</file>
|
||||||
|
<file leaf-file-name="Supervisor.scala" pinned="false" current="false" current-in-tab="false">
|
||||||
|
<entry file="file://$PROJECT_DIR$/akka-actors/src/main/scala/actor/Supervisor.scala">
|
||||||
|
<provider selected="true" editor-type-id="text-editor">
|
||||||
|
<state line="106" column="60" selection-start="3768" selection-end="3768" vertical-scroll-proportion="0.0">
|
||||||
|
<folding />
|
||||||
|
</state>
|
||||||
|
</provider>
|
||||||
|
</entry>
|
||||||
|
</file>
|
||||||
|
<file leaf-file-name="AMQP.scala" pinned="false" current="false" current-in-tab="false">
|
||||||
|
<entry file="file://$PROJECT_DIR$/akka-amqp/src/main/scala/AMQP.scala">
|
||||||
|
<provider selected="true" editor-type-id="text-editor">
|
||||||
|
<state line="291" column="0" selection-start="10068" selection-end="10068" vertical-scroll-proportion="0.0">
|
||||||
|
<folding />
|
||||||
|
</state>
|
||||||
|
</provider>
|
||||||
|
</entry>
|
||||||
|
</file>
|
||||||
|
<file leaf-file-name="Security.scala" pinned="false" current="true" current-in-tab="true">
|
||||||
|
<entry file="file://$PROJECT_DIR$/akka-security/src/main/scala/Security.scala">
|
||||||
|
<provider selected="true" editor-type-id="text-editor">
|
||||||
|
<state line="38" column="3" selection-start="1677" selection-end="1677" vertical-scroll-proportion="0.02631579">
|
||||||
|
<folding />
|
||||||
|
</state>
|
||||||
|
</provider>
|
||||||
|
</entry>
|
||||||
|
</file>
|
||||||
|
<file leaf-file-name="SecuritySpec.scala" pinned="false" current="false" current-in-tab="false">
|
||||||
|
<entry file="file://$PROJECT_DIR$/akka-security/src/test/scala/SecuritySpec.scala">
|
||||||
|
<provider selected="true" editor-type-id="text-editor">
|
||||||
|
<state line="65" column="45" selection-start="2449" selection-end="2449" vertical-scroll-proportion="0.0">
|
||||||
<folding />
|
<folding />
|
||||||
</state>
|
</state>
|
||||||
</provider>
|
</provider>
|
||||||
|
|
@ -90,6 +147,18 @@
|
||||||
<setting name="OPEN_NEW_TAB" value="false" />
|
<setting name="OPEN_NEW_TAB" value="false" />
|
||||||
</FindUsagesManager>
|
</FindUsagesManager>
|
||||||
</component>
|
</component>
|
||||||
|
<component name="IdeDocumentHistory">
|
||||||
|
<option name="changedFiles">
|
||||||
|
<list>
|
||||||
|
<option value="$PROJECT_DIR$/akka-actors/src/main/scala/actor/Actor.scala" />
|
||||||
|
<option value="$PROJECT_DIR$/akka-actors/src/main/scala/actor/Scheduler.scala" />
|
||||||
|
<option value="$PROJECT_DIR$/akka-actors/src/main/scala/actor/Supervisor.scala" />
|
||||||
|
<option value="$PROJECT_DIR$/akka-amqp/src/main/scala/AMQP.scala" />
|
||||||
|
<option value="$PROJECT_DIR$/akka-security/src/test/scala/SecuritySpec.scala" />
|
||||||
|
<option value="$PROJECT_DIR$/akka-security/src/main/scala/Security.scala" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
<component name="MavenImportPreferences">
|
<component name="MavenImportPreferences">
|
||||||
<option name="importingSettings">
|
<option name="importingSettings">
|
||||||
<MavenImportingSettings>
|
<MavenImportingSettings>
|
||||||
|
|
@ -173,6 +242,13 @@
|
||||||
<option name="Maven.BeforeRunTask" enabled="false" />
|
<option name="Maven.BeforeRunTask" enabled="false" />
|
||||||
</method>
|
</method>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
<configuration default="true" type="PHPUnitRunConfigurationType" factoryName="PHPUnit">
|
||||||
|
<method>
|
||||||
|
<option name="AntTarget" enabled="false" />
|
||||||
|
<option name="BuildArtifacts" enabled="false" />
|
||||||
|
<option name="Maven.BeforeRunTask" enabled="false" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
<configuration default="true" type="Applet" factoryName="Applet">
|
<configuration default="true" type="Applet" factoryName="Applet">
|
||||||
<module name="" />
|
<module name="" />
|
||||||
<option name="MAIN_CLASS_NAME" />
|
<option name="MAIN_CLASS_NAME" />
|
||||||
|
|
@ -329,9 +405,51 @@
|
||||||
</state>
|
</state>
|
||||||
</provider>
|
</provider>
|
||||||
</entry>
|
</entry>
|
||||||
|
<entry file="file://$PROJECT_DIR$/akka-actors/src/main/scala/actor/Scheduler.scala">
|
||||||
|
<provider selected="true" editor-type-id="text-editor">
|
||||||
|
<state line="45" column="0" selection-start="1590" selection-end="1628" vertical-scroll-proportion="0.0">
|
||||||
|
<folding />
|
||||||
|
</state>
|
||||||
|
</provider>
|
||||||
|
</entry>
|
||||||
|
<entry file="file://$PROJECT_DIR$/akka-actors/src/main/scala/config/Config.scala">
|
||||||
|
<provider selected="true" editor-type-id="text-editor">
|
||||||
|
<state line="22" column="13" selection-start="649" selection-end="649" vertical-scroll-proportion="0.0">
|
||||||
|
<folding />
|
||||||
|
</state>
|
||||||
|
</provider>
|
||||||
|
</entry>
|
||||||
|
<entry file="file://$PROJECT_DIR$/akka-actors/src/main/scala/actor/Supervisor.scala">
|
||||||
|
<provider selected="true" editor-type-id="text-editor">
|
||||||
|
<state line="106" column="60" selection-start="3768" selection-end="3768" vertical-scroll-proportion="0.0">
|
||||||
|
<folding />
|
||||||
|
</state>
|
||||||
|
</provider>
|
||||||
|
</entry>
|
||||||
|
<entry file="file://$PROJECT_DIR$/akka-amqp/src/main/scala/AMQP.scala">
|
||||||
|
<provider selected="true" editor-type-id="text-editor">
|
||||||
|
<state line="291" column="0" selection-start="10068" selection-end="10068" vertical-scroll-proportion="0.0">
|
||||||
|
<folding />
|
||||||
|
</state>
|
||||||
|
</provider>
|
||||||
|
</entry>
|
||||||
<entry file="file://$PROJECT_DIR$/akka-actors/src/main/scala/actor/Actor.scala">
|
<entry file="file://$PROJECT_DIR$/akka-actors/src/main/scala/actor/Actor.scala">
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
<provider selected="true" editor-type-id="text-editor">
|
||||||
<state line="162" column="71" selection-start="6288" selection-end="6288" vertical-scroll-proportion="1.7435898">
|
<state line="75" column="6" selection-start="2861" selection-end="2861" vertical-scroll-proportion="0.0">
|
||||||
|
<folding />
|
||||||
|
</state>
|
||||||
|
</provider>
|
||||||
|
</entry>
|
||||||
|
<entry file="file://$PROJECT_DIR$/akka-security/src/test/scala/SecuritySpec.scala">
|
||||||
|
<provider selected="true" editor-type-id="text-editor">
|
||||||
|
<state line="65" column="45" selection-start="2449" selection-end="2449" vertical-scroll-proportion="0.0">
|
||||||
|
<folding />
|
||||||
|
</state>
|
||||||
|
</provider>
|
||||||
|
</entry>
|
||||||
|
<entry file="file://$PROJECT_DIR$/akka-security/src/main/scala/Security.scala">
|
||||||
|
<provider selected="true" editor-type-id="text-editor">
|
||||||
|
<state line="38" column="3" selection-start="1677" selection-end="1677" vertical-scroll-proportion="0.02631579">
|
||||||
<folding />
|
<folding />
|
||||||
</state>
|
</state>
|
||||||
</provider>
|
</provider>
|
||||||
|
|
|
||||||
|
|
@ -124,10 +124,10 @@ trait Actor extends Logging with TransactionManagement {
|
||||||
* so it fits the specific use-case that the actor is used for.
|
* so it fits the specific use-case that the actor is used for.
|
||||||
* <p/>
|
* <p/>
|
||||||
* It is beneficial to have actors share the same dispatcher, easily +100 actors can share the same.
|
* It is beneficial to have actors share the same dispatcher, easily +100 actors can share the same.
|
||||||
* <br/>
|
* <p/>
|
||||||
* But if you are running many many actors then it can be a good idea to have split them up in terms of
|
* But if you are running many many actors then it can be a good idea to have split them up in terms of
|
||||||
* dispatcher sharing.
|
* dispatcher sharing.
|
||||||
* <br/>
|
* <p/>
|
||||||
* Default is that all actors that are created and spawned from within this actor is sharing the same
|
* Default is that all actors that are created and spawned from within this actor is sharing the same
|
||||||
* dispatcher as its creator.
|
* dispatcher as its creator.
|
||||||
* <pre>
|
* <pre>
|
||||||
|
|
@ -151,9 +151,19 @@ trait Actor extends Logging with TransactionManagement {
|
||||||
/**
|
/**
|
||||||
* User overridable callback/setting.
|
* User overridable callback/setting.
|
||||||
*
|
*
|
||||||
* Set trapExit to true if actor should be able to trap linked actors exit messages.
|
* Set trapExit to the list of exception classes that the actor should be able to trap
|
||||||
|
* from the actor it is supervising. When the supervising actor throws these exceptions
|
||||||
|
* then they will trigger a restart.
|
||||||
|
* <p/>
|
||||||
|
* <pre>
|
||||||
|
* // trap all exceptions
|
||||||
|
* trapExit = List(classOf[Throwable])
|
||||||
|
*
|
||||||
|
* // trap specific exceptions only
|
||||||
|
* trapExit = List(classOf[MyApplicationException], classOf[MyApplicationError])
|
||||||
|
* </pre>
|
||||||
*/
|
*/
|
||||||
protected[this] var trapExit: Boolean = false
|
protected[this] var trapExit: List[Class[_ <: Throwable]] = Nil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User overridable callback/setting.
|
* User overridable callback/setting.
|
||||||
|
|
@ -392,8 +402,9 @@ trait Actor extends Logging with TransactionManagement {
|
||||||
* Links an other actor to this actor. Links are unidirectional and means that a the linking actor will
|
* Links an other actor to this actor. Links are unidirectional and means that a the linking actor will
|
||||||
* receive a notification if the linked actor has crashed.
|
* receive a notification if the linked actor has crashed.
|
||||||
* <p/>
|
* <p/>
|
||||||
* If the 'trapExit' member field has been set to 'true' then it will 'trap' the failure and automatically
|
* If the 'trapExit' member field has been set to at contain at least one exception class then it will
|
||||||
* restart the linked actors according to the restart strategy defined by the 'faultHandler'.
|
* 'trap' these exceptions and automatically restart the linked actors according to the restart strategy
|
||||||
|
* defined by the 'faultHandler'.
|
||||||
* <p/>
|
* <p/>
|
||||||
* To be invoked from within the actor itself.
|
* To be invoked from within the actor itself.
|
||||||
*/
|
*/
|
||||||
|
|
@ -628,14 +639,14 @@ trait Actor extends Logging with TransactionManagement {
|
||||||
}
|
}
|
||||||
|
|
||||||
private[this] def handleTrapExit(dead: Actor, reason: Throwable): Unit = {
|
private[this] def handleTrapExit(dead: Actor, reason: Throwable): Unit = {
|
||||||
if (trapExit) {
|
if (trapExit.exists(_.isAssignableFrom(reason.getClass))) {
|
||||||
if (faultHandler.isDefined) {
|
if (faultHandler.isDefined) {
|
||||||
faultHandler.get match {
|
faultHandler.get match {
|
||||||
// FIXME: implement support for maxNrOfRetries and withinTimeRange in RestartStrategy
|
// FIXME: implement support for maxNrOfRetries and withinTimeRange in RestartStrategy
|
||||||
case AllForOneStrategy(maxNrOfRetries, withinTimeRange) => restartLinkedActors(reason)
|
case AllForOneStrategy(maxNrOfRetries, withinTimeRange) => restartLinkedActors(reason)
|
||||||
case OneForOneStrategy(maxNrOfRetries, withinTimeRange) => dead.restart(reason)
|
case OneForOneStrategy(maxNrOfRetries, withinTimeRange) => dead.restart(reason)
|
||||||
}
|
}
|
||||||
} else throw new IllegalStateException("No 'faultHandler' defined for actor with the 'trapExit' flag set to true - can't proceed " + toString)
|
} else throw new IllegalStateException("No 'faultHandler' defined for actor with the 'trapExit' member field set to non-empty list of exception classes - can't proceed " + toString)
|
||||||
} else {
|
} else {
|
||||||
if (_supervisor.isDefined) _supervisor.get ! Exit(dead, reason) // if 'trapExit' is not defined then pass the Exit on
|
if (_supervisor.isDefined) _supervisor.get ! Exit(dead, reason) // if 'trapExit' is not defined then pass the Exit on
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ object Scheduler extends Actor {
|
||||||
private var service = Executors.newSingleThreadScheduledExecutor(SchedulerThreadFactory)
|
private var service = Executors.newSingleThreadScheduledExecutor(SchedulerThreadFactory)
|
||||||
private val schedulers = new ConcurrentHashMap[Actor, Actor]
|
private val schedulers = new ConcurrentHashMap[Actor, Actor]
|
||||||
faultHandler = Some(OneForOneStrategy(5, 5000))
|
faultHandler = Some(OneForOneStrategy(5, 5000))
|
||||||
trapExit = true
|
trapExit = List(classOf[Throwable])
|
||||||
start
|
start
|
||||||
|
|
||||||
def schedule(receiver: Actor, message: AnyRef, initialDelay: Long, delay: Long, timeUnit: TimeUnit) = {
|
def schedule(receiver: Actor, message: AnyRef, initialDelay: Long, delay: Long, timeUnit: TimeUnit) = {
|
||||||
|
|
|
||||||
|
|
@ -99,12 +99,12 @@ abstract class SupervisorFactory extends Logging {
|
||||||
* Should not be used in development. Instead wire the actors together using 'link', 'spawnLink' etc. and set the 'trapExit'
|
* Should not be used in development. Instead wire the actors together using 'link', 'spawnLink' etc. and set the 'trapExit'
|
||||||
* flag in the actors that should trap error signals and trigger restart.
|
* flag in the actors that should trap error signals and trigger restart.
|
||||||
* <p/>
|
* <p/>
|
||||||
* See the ScalaDoc for the SupervisorFactory for an example on how to declaratively wire up actors.
|
* See the ScalaDoc for the SupervisorFactory for an example on how to declaratively wire up actors.
|
||||||
*
|
*
|
||||||
* @author <a href="http://jonasboner.com">Jonas Bonér</a>
|
* @author <a href="http://jonasboner.com">Jonas Bonér</a>
|
||||||
*/
|
*/
|
||||||
class Supervisor private[akka] (handler: FaultHandlingStrategy) extends Actor with Logging with Configurator {
|
class Supervisor private[akka] (handler: FaultHandlingStrategy) extends Actor with Logging with Configurator {
|
||||||
trapExit = true
|
trapExit = List(classOf[Throwable])
|
||||||
faultHandler = Some(handler)
|
faultHandler = Some(handler)
|
||||||
//dispatcher = Dispatchers.newThreadBasedDispatcher(this)
|
//dispatcher = Dispatchers.newThreadBasedDispatcher(this)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ import java.io.IOException
|
||||||
object AMQP extends Actor {
|
object AMQP extends Actor {
|
||||||
private val connections = new ConcurrentHashMap[FaultTolerantConnectionActor, FaultTolerantConnectionActor]
|
private val connections = new ConcurrentHashMap[FaultTolerantConnectionActor, FaultTolerantConnectionActor]
|
||||||
faultHandler = Some(OneForOneStrategy(5, 5000))
|
faultHandler = Some(OneForOneStrategy(5, 5000))
|
||||||
trapExit = true
|
trapExit = List(classOf[Throwable])
|
||||||
start
|
start
|
||||||
|
|
||||||
sealed trait AMQPMessage
|
sealed trait AMQPMessage
|
||||||
|
|
@ -288,7 +288,7 @@ object AMQP extends Actor {
|
||||||
extends FaultTolerantConnectionActor { self: Consumer =>
|
extends FaultTolerantConnectionActor { self: Consumer =>
|
||||||
|
|
||||||
faultHandler = Some(OneForOneStrategy(5, 5000))
|
faultHandler = Some(OneForOneStrategy(5, 5000))
|
||||||
trapExit = true
|
trapExit = List(classOf[Throwable])
|
||||||
|
|
||||||
private val listeners = new HashMap[MessageConsumerListener, MessageConsumerListener]
|
private val listeners = new HashMap[MessageConsumerListener, MessageConsumerListener]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,20 +22,20 @@
|
||||||
|
|
||||||
package se.scalablesolutions.akka.security
|
package se.scalablesolutions.akka.security
|
||||||
|
|
||||||
import _root_.se.scalablesolutions.akka.actor.{Scheduler,Actor,ActorRegistry}
|
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_.se.scalablesolutions.akka.util.{Logging}
|
import _root_.se.scalablesolutions.akka.Config
|
||||||
|
|
||||||
import _root_.com.sun.jersey.api.model.AbstractMethod
|
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.spi.container.{ResourceFilterFactory, ContainerRequest, ContainerRequestFilter, ContainerResponse, ContainerResponseFilter, ResourceFilter}
|
||||||
import _root_.com.sun.jersey.core.util.Base64
|
import _root_.com.sun.jersey.core.util.Base64
|
||||||
import _root_.javax.ws.rs.core.{SecurityContext,Context,Response}
|
import _root_.javax.ws.rs.core.{SecurityContext, Context, Response}
|
||||||
import _root_.javax.ws.rs.WebApplicationException
|
import _root_.javax.ws.rs.WebApplicationException
|
||||||
import _root_.javax.annotation.security.{DenyAll,PermitAll,RolesAllowed}
|
import _root_.javax.annotation.security.{DenyAll, PermitAll, RolesAllowed}
|
||||||
import _root_.java.security.Principal
|
import _root_.java.security.Principal
|
||||||
import _root_.java.util.concurrent.TimeUnit
|
import _root_.java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
import _root_.net.liftweb.util.{SecurityHelpers, StringHelpers,IoHelpers}
|
import _root_.net.liftweb.util.{SecurityHelpers, StringHelpers, IoHelpers}
|
||||||
|
|
||||||
object Enc extends SecurityHelpers with StringHelpers with IoHelpers
|
object Enc extends SecurityHelpers with StringHelpers with IoHelpers
|
||||||
|
|
||||||
|
|
@ -44,18 +44,16 @@ case object OK
|
||||||
/**
|
/**
|
||||||
* Authenticate represents a message to authenticate a request
|
* Authenticate represents a message to authenticate a request
|
||||||
*/
|
*/
|
||||||
case class Authenticate(val req : ContainerRequest, val rolesAllowed : List[String])
|
case class Authenticate(val req: ContainerRequest, val rolesAllowed: List[String])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User info represents a sign-on with associated credentials/roles
|
* User info represents a sign-on with associated credentials/roles
|
||||||
*/
|
*/
|
||||||
case class UserInfo(val username : String,val password : String,val roles : List[String])
|
case class UserInfo(val username: String, val password: String, val roles: List[String])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
trait Credentials
|
trait Credentials
|
||||||
|
|
||||||
case class BasicCredentials(username : String, password : String) extends Credentials
|
case class BasicCredentials(username: String, password: String) extends Credentials
|
||||||
|
|
||||||
case class DigestCredentials(method: String,
|
case class DigestCredentials(method: String,
|
||||||
userName: String,
|
userName: String,
|
||||||
|
|
@ -68,151 +66,145 @@ case class DigestCredentials(method: String,
|
||||||
response: String,
|
response: String,
|
||||||
opaque: String) extends Credentials
|
opaque: String) extends Credentials
|
||||||
|
|
||||||
case class SpnegoCredentials(token : Array[Byte]) extends Credentials
|
case class SpnegoCredentials(token: Array[Byte]) extends Credentials
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Jersey Filter for invocation intercept and authorization/authentication
|
* Jersey Filter for invocation intercept and authorization/authentication
|
||||||
*/
|
*/
|
||||||
class AkkaSecurityFilterFactory extends ResourceFilterFactory with Logging {
|
class AkkaSecurityFilterFactory extends ResourceFilterFactory with Logging {
|
||||||
|
class Filter(actor: Actor, rolesAllowed: Option[List[String]])
|
||||||
|
extends ResourceFilter with ContainerRequestFilter with Logging {
|
||||||
|
|
||||||
class Filter(actor : Actor,rolesAllowed : Option[List[String]]) extends ResourceFilter with ContainerRequestFilter with Logging {
|
override def getRequestFilter: ContainerRequestFilter = this
|
||||||
|
|
||||||
override def getRequestFilter : ContainerRequestFilter = this
|
override def getResponseFilter: ContainerResponseFilter = null
|
||||||
override def getResponseFilter : ContainerResponseFilter = null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Here's where the magic happens. The request is authenticated by
|
* Here's where the magic happens. The request is authenticated by
|
||||||
* sending a request for authentication to the configured authenticator actor
|
* sending a request for authentication to the configured authenticator actor
|
||||||
*/
|
*/
|
||||||
override def filter(request : ContainerRequest) : ContainerRequest =
|
override def filter(request: ContainerRequest): ContainerRequest =
|
||||||
rolesAllowed match {
|
rolesAllowed match {
|
||||||
case Some(roles) => {
|
case Some(roles) => {
|
||||||
|
(authenticator !? Authenticate(request, roles)).asInstanceOf[AnyRef] match {
|
||||||
val result : AnyRef = (authenticator !? Authenticate(request,roles))
|
case OK => request
|
||||||
|
case r if r.isInstanceOf[Response] =>
|
||||||
result match {
|
throw new WebApplicationException(r.asInstanceOf[Response])
|
||||||
case OK => request
|
case x => {
|
||||||
case r if r.isInstanceOf[Response] =>
|
log.error("Authenticator replied with unexpected result: ", x);
|
||||||
throw new WebApplicationException(r.asInstanceOf[Response])
|
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
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"))
|
lazy val authenticatorFQN = 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
|
* 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
|
* strategy could be implemented in the future
|
||||||
*/
|
*/
|
||||||
def authenticator : Actor = ActorRegistry.actorsFor(authenticatorFQN).head
|
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))
|
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
|
* The create method is invoked for each resource, and we look for javax.annotation.security annotations
|
||||||
* and create the appropriate Filter configurations for each.
|
* and create the appropriate Filter configurations for each.
|
||||||
*/
|
*/
|
||||||
override def create(am : AbstractMethod) : java.util.List[ResourceFilter] = {
|
override def create(am: AbstractMethod): java.util.List[ResourceFilter] = {
|
||||||
|
|
||||||
//DenyAll takes precedence
|
//DenyAll takes precedence
|
||||||
if (am.isAnnotationPresent(classOf[DenyAll]))
|
if (am.isAnnotationPresent(classOf[DenyAll]))
|
||||||
return mkFilter(None)
|
return mkFilter(None)
|
||||||
|
|
||||||
//Method-level RolesAllowed takes precedence
|
//Method-level RolesAllowed takes precedence
|
||||||
val ra = am.getAnnotation(classOf[RolesAllowed])
|
val ra = am.getAnnotation(classOf[RolesAllowed])
|
||||||
|
|
||||||
if (ra ne null)
|
if (ra ne null)
|
||||||
return mkFilter(Some(ra.value.toList))
|
return mkFilter(Some(ra.value.toList))
|
||||||
|
|
||||||
//PermitAll takes precedence over resource-level RolesAllowed annotation
|
//PermitAll takes precedence over resource-level RolesAllowed annotation
|
||||||
if (am.isAnnotationPresent(classOf[PermitAll]))
|
if (am.isAnnotationPresent(classOf[PermitAll]))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
//Last but not least, the resource-level RolesAllowed
|
//Last but not least, the resource-level RolesAllowed
|
||||||
val cra = am.getResource.getAnnotation(classOf[RolesAllowed])
|
val cra = am.getResource.getAnnotation(classOf[RolesAllowed])
|
||||||
if (cra ne null)
|
if (cra ne null)
|
||||||
return mkFilter(Some(cra.value.toList))
|
return mkFilter(Some(cra.value.toList))
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AuthenticationActor is the super-trait for actors doing Http authentication
|
* AuthenticationActor is the super-trait for actors doing Http authentication
|
||||||
* It defines the common ground and the flow of execution
|
* It defines the common ground and the flow of execution
|
||||||
*/
|
*/
|
||||||
trait AuthenticationActor[C <: Credentials] extends Actor with Logging
|
trait AuthenticationActor[C <: Credentials] extends Actor {
|
||||||
{
|
type Req = ContainerRequest
|
||||||
type Req = ContainerRequest
|
|
||||||
|
|
||||||
//What realm does the authentication use?
|
//What realm does the authentication use?
|
||||||
def realm : String
|
def realm: String
|
||||||
|
|
||||||
//Creates a response to signal unauthorized
|
//Creates a response to signal unauthorized
|
||||||
def unauthorized : Response
|
def unauthorized: Response
|
||||||
|
|
||||||
//Used to extract information from the request, returns None if no credentials found
|
//Used to extract information from the request, returns None if no credentials found
|
||||||
def extractCredentials(r : Req) : Option[C]
|
def extractCredentials(r: Req): Option[C]
|
||||||
|
|
||||||
//returns None is unverified
|
//returns None is unverified
|
||||||
def verify(c : Option[C]) : Option[UserInfo]
|
def verify(c: Option[C]): Option[UserInfo]
|
||||||
|
|
||||||
//Contruct a new SecurityContext from the supplied parameters
|
//Contruct a new SecurityContext from the supplied parameters
|
||||||
def mkSecurityContext(r : Req, user : UserInfo) : SecurityContext
|
def mkSecurityContext(r: Req, user: UserInfo): SecurityContext
|
||||||
|
|
||||||
//This is the default security context factory
|
//This is the default security context factory
|
||||||
def mkDefaultSecurityContext(r : Req,u : UserInfo, scheme : String) : SecurityContext = {
|
def mkDefaultSecurityContext(r: Req, u: UserInfo, scheme: String): SecurityContext = {
|
||||||
val n = u.username
|
val n = u.username
|
||||||
val p = new Principal { def getName = n }
|
val p = new Principal {def getName = n}
|
||||||
|
|
||||||
new SecurityContext {
|
new SecurityContext {
|
||||||
def getAuthenticationScheme = scheme
|
def getAuthenticationScheme = scheme
|
||||||
def getUserPrincipal = p
|
def getUserPrincipal = p
|
||||||
def isSecure = r.isSecure
|
def isSecure = r.isSecure
|
||||||
def isUserInRole(role : String) = u.roles.exists(_ == role)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
def receive = authenticate
|
||||||
* 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))
|
//returns the string value of the "Authorization"-header of the request
|
||||||
|
def auth(r: Req) = r.getHeaderValue("Authorization")
|
||||||
|
|
||||||
if(roles.exists(req.isUserInRole(_)))
|
//Turns the aforementioned header value into an option
|
||||||
reply(OK)
|
def authOption(r: Req): Option[String] = {
|
||||||
else
|
val a = auth(r)
|
||||||
reply(Response.status(Response.Status.FORBIDDEN).build)
|
if (a != null && a.length > 0) Some(a) else None
|
||||||
}
|
}
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -220,127 +212,117 @@ trait AuthenticationActor[C <: Credentials] extends Actor with Logging
|
||||||
* mix this trait into a class to create an authenticator
|
* 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
|
* Don't forget to set the authenticator FQN in the rest-part of the akka config
|
||||||
*/
|
*/
|
||||||
trait BasicAuthenticationActor extends AuthenticationActor[BasicCredentials]
|
trait BasicAuthenticationActor extends AuthenticationActor[BasicCredentials] {
|
||||||
{
|
override def unauthorized =
|
||||||
override def unauthorized =
|
Response.status(401).header("WWW-Authenticate", "Basic realm=\"" + realm + "\"").build
|
||||||
Response.status(401).header("WWW-Authenticate","Basic realm=\"" + realm + "\"").build
|
|
||||||
|
|
||||||
override def extractCredentials(r : Req) : Option[BasicCredentials] = {
|
override def extractCredentials(r: Req): Option[BasicCredentials] = {
|
||||||
|
val Authorization = """(.*):(.*)""".r
|
||||||
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 =
|
authOption(r) match {
|
||||||
mkDefaultSecurityContext(r,u,SecurityContext.BASIC_AUTH)
|
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
|
* This trait implements the logic for Http Digest authentication mix this trait into a
|
||||||
* mix this trait into a class to create an authenticator
|
* class to create an authenticator. Don't forget to set the authenticator FQN in the
|
||||||
* Don't forget to set the authenticator FQN in the rest-part of the akka config
|
* rest-part of the akka config
|
||||||
*/
|
*/
|
||||||
trait DigestAuthenticationActor extends AuthenticationActor[DigestCredentials]
|
trait DigestAuthenticationActor extends AuthenticationActor[DigestCredentials] {
|
||||||
{
|
import Enc._
|
||||||
import Enc._
|
|
||||||
|
|
||||||
private object InvalidateNonces
|
private object InvalidateNonces
|
||||||
|
|
||||||
//Holds the generated nonces for the specified validity period
|
//Holds the generated nonces for the specified validity period
|
||||||
val nonceMap = mkNonceMap
|
val nonceMap = mkNonceMap
|
||||||
|
|
||||||
//Discards old nonces
|
//Discards old nonces
|
||||||
protected val invalidateNonces: PartialFunction[Any,Unit] = {
|
protected val invalidateNonces: PartialFunction[Any, Unit] = {
|
||||||
case InvalidateNonces =>
|
case InvalidateNonces =>
|
||||||
{
|
val ts = System.currentTimeMillis
|
||||||
val ts = System.currentTimeMillis
|
nonceMap.retain((k, v) => (ts - v) < nonceValidityPeriod)
|
||||||
|
case e =>
|
||||||
|
log.info("Don't know what to do with: " + e)
|
||||||
|
}
|
||||||
|
|
||||||
nonceMap.retain((k,v) => (ts - v) < nonceValidityPeriod)
|
//Schedule the invalidation of nonces
|
||||||
}
|
Scheduler.schedule(this, InvalidateNonces, noncePurgeInterval, noncePurgeInterval, TimeUnit.MILLISECONDS)
|
||||||
|
|
||||||
case e => log.info("Don't know what to do with: " + e)
|
//authenticate or invalidate nonces
|
||||||
}
|
override def receive = authenticate orElse invalidateNonces
|
||||||
|
|
||||||
//Schedule the invalidation of nonces
|
override def unauthorized: Response = {
|
||||||
Scheduler.schedule(this, InvalidateNonces, noncePurgeInterval, noncePurgeInterval, TimeUnit.MILLISECONDS )
|
val nonce = randomString(64);
|
||||||
|
nonceMap.put(nonce, System.currentTimeMillis)
|
||||||
|
unauthorized(nonce, "auth", randomString(64))
|
||||||
|
}
|
||||||
|
|
||||||
//authenticate or invalidate nonces
|
def unauthorized(nonce: String, qop: String, opaque: String): Response = {
|
||||||
override def receive: PartialFunction[Any, Unit] = authenticate orElse invalidateNonces
|
Response.status(401).header("WWW-Authenticate",
|
||||||
|
"Digest realm=\"" + realm + "\", " +
|
||||||
|
"qop=\"" + qop + "\", " +
|
||||||
|
"nonce=\"" + nonce + "\", " +
|
||||||
|
"opaque=\"" + opaque + "\"").build
|
||||||
|
}
|
||||||
|
|
||||||
override def unauthorized : Response =
|
//Tests wether the specified credentials are valid
|
||||||
{
|
def validate(auth: DigestCredentials, user: UserInfo): Boolean = {
|
||||||
val nonce = randomString(64);
|
def h(s: String) = hexEncode(md5(s.getBytes("UTF-8")))
|
||||||
nonceMap.put(nonce,System.currentTimeMillis)
|
|
||||||
unauthorized(nonce,"auth",randomString(64))
|
|
||||||
}
|
|
||||||
|
|
||||||
def unauthorized(nonce : String, qop : String, opaque : String) : Response =
|
val ha1 = h(auth.userName + ":" + auth.realm + ":" + user.password)
|
||||||
{
|
val ha2 = h(auth.method + ":" + auth.uri)
|
||||||
Response.status(401).header("WWW-Authenticate",
|
|
||||||
"Digest realm=\"" + realm + "\", " +
|
|
||||||
"qop=\"" + qop + "\", " +
|
|
||||||
"nonce=\"" + nonce + "\", " +
|
|
||||||
"opaque=\"" + opaque + "\"").build
|
|
||||||
}
|
|
||||||
|
|
||||||
//Tests wether the specified credentials are valid
|
val response = h(ha1 + ":" + auth.nonce + ":" +
|
||||||
def validate(auth: DigestCredentials,user : UserInfo) : Boolean = {
|
auth.nc + ":" + auth.cnonce + ":" +
|
||||||
def h(s : String) = hexEncode(md5(s.getBytes("UTF-8")))
|
auth.qop + ":" + ha2)
|
||||||
|
|
||||||
val ha1 = h(auth.userName + ":" + auth.realm + ":" + user.password)
|
(response == auth.response) && (nonceMap.getOrElse(auth.nonce, -1) != -1)
|
||||||
val ha2 = h(auth.method + ":" + auth.uri)
|
}
|
||||||
|
|
||||||
val response = h(ha1 + ":" + auth.nonce + ":" +
|
override def verify(odc: Option[DigestCredentials]): Option[UserInfo] = odc match {
|
||||||
auth.nc + ":" + auth.cnonce + ":" +
|
case Some(dc) => {
|
||||||
auth.qop + ":" + ha2)
|
userInfo(dc.userName) match {
|
||||||
|
case Some(u) if validate(dc, u) =>
|
||||||
(response == auth.response) && (nonceMap.getOrElse(auth.nonce, -1) != -1)
|
nonceMap.get(dc.nonce).map(t => (System.currentTimeMillis - t) < nonceValidityPeriod).map(_ => u)
|
||||||
}
|
|
||||||
|
|
||||||
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
|
case _ => None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
|
||||||
override def extractCredentials(r : Req) : Option[DigestCredentials] =
|
override def extractCredentials(r: Req): Option[DigestCredentials] = {
|
||||||
{
|
authOption(r).map(s => {
|
||||||
authOption(r).map( s => {
|
val ? = splitNameValuePairs(s.substring(7, s.length))
|
||||||
val ? = splitNameValuePairs(s.substring(7,s.length ))
|
DigestCredentials(r.getMethod.toUpperCase,
|
||||||
|
?("username"), ?("realm"), ?("nonce"),
|
||||||
|
?("uri"), ?("qop"), ?("nc"),
|
||||||
|
?("cnonce"), ?("response"), ?("opaque"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
DigestCredentials(r.getMethod.toUpperCase, ?("username"), ?("realm"), ?("nonce"),
|
override def mkSecurityContext(r: Req, u: UserInfo): SecurityContext =
|
||||||
?("uri"), ?("qop"), ?("nc"),
|
mkDefaultSecurityContext(r, u, SecurityContext.DIGEST_AUTH)
|
||||||
?("cnonce"), ?("response"), ?("opaque"))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override def mkSecurityContext(r : Req,u : UserInfo) : SecurityContext =
|
//Mandatory overrides
|
||||||
mkDefaultSecurityContext(r,u,SecurityContext.DIGEST_AUTH)
|
def userInfo(username: String): Option[UserInfo]
|
||||||
|
|
||||||
//Mandatory overrides
|
def mkNonceMap: scala.collection.mutable.Map[String, Long]
|
||||||
def userInfo(username : String) : Option[UserInfo]
|
|
||||||
|
|
||||||
def mkNonceMap : scala.collection.mutable.Map[String,Long]
|
//Optional overrides
|
||||||
|
def nonceValidityPeriod = 60 * 1000 //ms
|
||||||
//Optional overrides
|
def noncePurgeInterval = 2 * 60 * 1000 //ms
|
||||||
def nonceValidityPeriod = 60*1000//ms
|
|
||||||
def noncePurgeInterval = 2*60*1000 //ms
|
|
||||||
}
|
}
|
||||||
|
|
||||||
import _root_.java.security.Principal
|
import _root_.java.security.Principal
|
||||||
|
|
@ -356,143 +338,126 @@ import _root_.javax.security.auth.kerberos.KerberosPrincipal
|
||||||
import _root_.org.ietf.jgss.GSSContext
|
import _root_.org.ietf.jgss.GSSContext
|
||||||
import _root_.org.ietf.jgss.GSSCredential
|
import _root_.org.ietf.jgss.GSSCredential
|
||||||
import _root_.org.ietf.jgss.GSSManager
|
import _root_.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
|
trait SpnegoAuthenticationActor extends AuthenticationActor[SpnegoCredentials] {
|
||||||
// but the commons Base64 does
|
override def unauthorized =
|
||||||
import _root_.org.apache.commons.codec.binary.Base64
|
Response.status(401).header("WWW-Authenticate", "Negotiate").build
|
||||||
override def extractCredentials(r : Req) : Option[SpnegoCredentials] = {
|
|
||||||
|
|
||||||
val AuthHeader = """Negotiate\s(.*)""".r
|
// for some reason the jersey Base64 class does not work with kerberos
|
||||||
|
// but the commons Base64 does
|
||||||
|
import _root_.org.apache.commons.codec.binary.Base64
|
||||||
|
override def extractCredentials(r: Req): Option[SpnegoCredentials] = {
|
||||||
|
val AuthHeader = """Negotiate\s(.*)""".r
|
||||||
|
|
||||||
authOption(r) match {
|
authOption(r) match {
|
||||||
case Some(AuthHeader(token)) => {
|
case Some(AuthHeader(token)) =>
|
||||||
Some(SpnegoCredentials(Base64.decodeBase64(token.trim.getBytes)))
|
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 => {
|
|
||||||
e.printStackTrace
|
|
||||||
return None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case _ => 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"
|
|
||||||
|
|
||||||
/**
|
override def verify(odc: Option[SpnegoCredentials]): Option[UserInfo] = odc match {
|
||||||
* returns the roles for the given user
|
case Some(dc) => {
|
||||||
*/
|
try {
|
||||||
def rolesFor(user: String): List[String]
|
val principal = Subject.doAs(this.serviceSubject, new KerberosValidateAction(dc.token));
|
||||||
|
val user = stripRealmFrom(principal)
|
||||||
// Kerberos
|
Some(UserInfo(user, null, rolesFor(user)))
|
||||||
|
} catch {
|
||||||
/**
|
case e: PrivilegedActionException => {
|
||||||
* strips the realm from a kerberos principal name, returning only the user part
|
e.printStackTrace
|
||||||
*/
|
return None
|
||||||
private def stripRealmFrom(principal: String):String = principal.split("@")(0)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* principal name for the HTTP kerberos service, i.e HTTP/{server}@{realm}
|
|
||||||
*/
|
|
||||||
lazy val servicePrincipal = akka.Config.config.getString("akka.rest.kerberos.servicePrincipal").getOrElse(throw new IllegalStateException("akka.rest.kerberos.servicePrincipal"))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* keytab location with credentials for the service principal
|
|
||||||
*/
|
|
||||||
lazy val keyTabLocation = akka.Config.config.getString("akka.rest.kerberos.keyTabLocation").getOrElse(throw new IllegalStateException("akka.rest.kerberos.keyTabLocation"))
|
|
||||||
|
|
||||||
lazy val kerberosDebug = akka.Config.config.getString("akka.rest.kerberos.kerberosDebug").getOrElse("false")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* is not used by this authenticator, so accept an empty value
|
|
||||||
*/
|
|
||||||
lazy val realm = akka.Config.config.getString("akka.rest.kerberos.realm").getOrElse("")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
|
||||||
// service principal login to kerberos on startup
|
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"
|
||||||
|
|
||||||
val serviceSubject = servicePrincipalLogin
|
/**
|
||||||
|
* returns the roles for the given user
|
||||||
|
*/
|
||||||
|
def rolesFor(user: String): List[String]
|
||||||
|
|
||||||
/**
|
// Kerberos
|
||||||
* 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))
|
* strips the realm from a kerberos principal name, returning only the user part
|
||||||
|
*/
|
||||||
|
private def stripRealmFrom(principal: String): String = principal.split("@")(0)
|
||||||
|
|
||||||
val sub = new Subject(false, princ, new java.util.HashSet[Object], new java.util.HashSet[Object])
|
/**
|
||||||
|
* principal name for the HTTP kerberos service, i.e HTTP/ { server } @ { realm }
|
||||||
|
*/
|
||||||
|
lazy val servicePrincipal = Config.config.getString("akka.rest.kerberos.servicePrincipal").getOrElse(throw new IllegalStateException("akka.rest.kerberos.servicePrincipal"))
|
||||||
|
|
||||||
val lc = new LoginContext("", sub, null, loginConfig)
|
/**
|
||||||
|
* keytab location with credentials for the service principal
|
||||||
lc.login()
|
*/
|
||||||
|
lazy val keyTabLocation = Config.config.getString("akka.rest.kerberos.keyTabLocation").getOrElse(throw new IllegalStateException("akka.rest.kerberos.keyTabLocation"))
|
||||||
|
|
||||||
lc.getSubject()
|
lazy val kerberosDebug = Config.config.getString("akka.rest.kerberos.kerberosDebug").getOrElse("false")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* is not used by this authenticator, so accept an empty value
|
||||||
|
*/
|
||||||
|
lazy val realm = Config.config.getString("akka.rest.kerberos.realm").getOrElse("")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
* 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 serviceSubject = servicePrincipalLogin
|
||||||
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",
|
* acquire an initial ticket from the kerberos server for the HTTP service
|
||||||
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
|
*/
|
||||||
options))
|
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))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,13 @@ import org.scalatest.mock.MockitoSugar
|
||||||
import org.mockito.Mockito._
|
import org.mockito.Mockito._
|
||||||
import org.mockito.Matchers._
|
import org.mockito.Matchers._
|
||||||
import org.junit.{Before, After, Test}
|
import org.junit.{Before, After, Test}
|
||||||
import _root_.javax.ws.rs.core.{SecurityContext,Context,Response}
|
|
||||||
import _root_.com.sun.jersey.spi.container.{ResourceFilterFactory,ContainerRequest,ContainerRequestFilter,ContainerResponse,ContainerResponseFilter,ResourceFilter}
|
import _root_.javax.ws.rs.core.{SecurityContext, Context, Response}
|
||||||
|
import _root_.com.sun.jersey.spi.container.{ResourceFilterFactory, ContainerRequest, ContainerRequestFilter, ContainerResponse, ContainerResponseFilter, ResourceFilter}
|
||||||
import _root_.com.sun.jersey.core.util.Base64
|
import _root_.com.sun.jersey.core.util.Base64
|
||||||
|
|
||||||
class BasicAuthenticatorSpec extends junit.framework.TestCase with Suite with MockitoSugar with MustMatchers {
|
class BasicAuthenticatorSpec extends junit.framework.TestCase
|
||||||
|
with Suite with MockitoSugar with MustMatchers {
|
||||||
val authenticator = new BasicAuthenticator
|
val authenticator = new BasicAuthenticator
|
||||||
authenticator.start
|
authenticator.start
|
||||||
|
|
||||||
|
|
@ -28,47 +29,46 @@ class BasicAuthenticatorSpec extends junit.framework.TestCase with Suite with Mo
|
||||||
val result: Response = (authenticator !? Authenticate(req, List("foo")))
|
val result: Response = (authenticator !? Authenticate(req, List("foo")))
|
||||||
|
|
||||||
// the actor replies with a challenge for the browser
|
// the actor replies with a challenge for the browser
|
||||||
result.getStatus must equal (Response.Status.UNAUTHORIZED.getStatusCode)
|
result.getStatus must equal(Response.Status.UNAUTHORIZED.getStatusCode)
|
||||||
result.getMetadata.get("WWW-Authenticate").get(0).toString must startWith ("Basic")
|
result.getMetadata.get("WWW-Authenticate").get(0).toString must startWith("Basic")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test def testAuthenticationSuccess = {
|
@Test def testAuthenticationSuccess = {
|
||||||
val req = mock[ContainerRequest]
|
val req = mock[ContainerRequest]
|
||||||
// fake a basic auth header -> this will authenticate the user
|
// fake a basic auth header -> this will authenticate the user
|
||||||
when(req.getHeaderValue("Authorization")).thenReturn("Basic " + new String(Base64.encode("foo:bar")))
|
when(req.getHeaderValue("Authorization")).thenReturn("Basic " + new String(Base64.encode("foo:bar")))
|
||||||
|
|
||||||
// fake a request authorization -> this will authorize the user
|
// fake a request authorization -> this will authorize the user
|
||||||
when(req.isUserInRole("chef")).thenReturn(true)
|
when(req.isUserInRole("chef")).thenReturn(true)
|
||||||
|
|
||||||
val result: AnyRef = (authenticator !? Authenticate(req, List("chef")))
|
val result: AnyRef = (authenticator !? Authenticate(req, List("chef")))
|
||||||
|
|
||||||
result must be (OK)
|
result must be(OK)
|
||||||
// the authenticator must have set a security context
|
// the authenticator must have set a security context
|
||||||
verify(req).setSecurityContext(any[SecurityContext])
|
verify(req).setSecurityContext(any[SecurityContext])
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test def testUnauthorized = {
|
@Test def testUnauthorized = {
|
||||||
val req = mock[ContainerRequest]
|
val req = mock[ContainerRequest]
|
||||||
|
|
||||||
// fake a basic auth header -> this will authenticate the user
|
// fake a basic auth header -> this will authenticate the user
|
||||||
when(req.getHeaderValue("Authorization")).thenReturn("Basic " + new String(Base64.encode("foo:bar")))
|
when(req.getHeaderValue("Authorization")).thenReturn("Basic " + new String(Base64.encode("foo:bar")))
|
||||||
when(req.isUserInRole("chef")).thenReturn(false) // this will deny access
|
when(req.isUserInRole("chef")).thenReturn(false) // this will deny access
|
||||||
|
|
||||||
val result: Response = (authenticator !? Authenticate(req, List("chef")))
|
val result: Response = (authenticator !? Authenticate(req, List("chef")))
|
||||||
|
|
||||||
result.getStatus must equal (Response.Status.FORBIDDEN.getStatusCode)
|
result.getStatus must equal(Response.Status.FORBIDDEN.getStatusCode)
|
||||||
// the authenticator must have set a security context
|
|
||||||
verify(req).setSecurityContext(any[SecurityContext])
|
|
||||||
|
|
||||||
|
// the authenticator must have set a security context
|
||||||
|
verify(req).setSecurityContext(any[SecurityContext])
|
||||||
}
|
}
|
||||||
|
|
||||||
class BasicAuthenticator extends BasicAuthenticationActor {
|
class BasicAuthenticator extends BasicAuthenticationActor {
|
||||||
|
def verify(odc: Option[BasicCredentials]): Option[UserInfo] = odc match {
|
||||||
def verify(odc : Option[BasicCredentials]) : Option[UserInfo] = odc match {
|
case Some(dc) => Some(UserInfo("foo", "bar", "ninja" :: "chef" :: Nil))
|
||||||
case Some(dc) => Some(UserInfo("foo","bar","ninja" :: "chef" :: Nil))
|
case _ => None
|
||||||
case _ => None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override def realm = "test"
|
override def realm = "test"
|
||||||
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue