You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@predictionio.apache.org by ap...@apache.org on 2016/06/27 19:31:30 UTC

[16/50] [abbrv] incubator-predictionio git commit: Add TLS and simple key-based auth to Dashboard and stop/restart endpoints

Add TLS and simple key-based auth to Dashboard and stop/restart
endpoints


Project: http://git-wip-us.apache.org/repos/asf/incubator-predictionio/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-predictionio/commit/71266230
Tree: http://git-wip-us.apache.org/repos/asf/incubator-predictionio/tree/71266230
Diff: http://git-wip-us.apache.org/repos/asf/incubator-predictionio/diff/71266230

Branch: refs/heads/master
Commit: 7126623051450256ad9af26210bf4441dbd4469e
Parents: acd9e3f
Author: Yevgeny <yk...@salesforce.com>
Authored: Tue Mar 1 19:41:09 2016 -0800
Committer: Yevgeny <yk...@salesforce.com>
Committed: Tue Mar 1 19:42:44 2016 -0800

----------------------------------------------------------------------
 common/build.sbt                                |   5 ++
 common/src/main/resources/application.conf      |  11 ++++
 .../authentication/Authentication.scala         |  50 ++++++++++++++++++
 .../configuration/SSLConfiguration.scala        |  52 +++++++++++++++++++
 conf/keystore.jks                               | Bin 0 -> 2238 bytes
 conf/server.conf                                |   9 ++++
 core/src/main/resources/application.conf        |   4 --
 .../io/prediction/workflow/CreateServer.scala   |  49 +++++++++++------
 .../io/prediction/data/api/EventServer.scala    |   3 +-
 .../prediction/tools/dashboard/Dashboard.scala  |  37 ++++++++-----
 10 files changed, 185 insertions(+), 35 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-predictionio/blob/71266230/common/build.sbt
----------------------------------------------------------------------
diff --git a/common/build.sbt b/common/build.sbt
index 2ebc176..9bda9c5 100644
--- a/common/build.sbt
+++ b/common/build.sbt
@@ -13,3 +13,8 @@
 // limitations under the License.
 
 name := "common"
+libraryDependencies ++= Seq(
+  "io.spray"               %% "spray-can"        % "1.3.2",
+  "io.spray"               %% "spray-routing"    % "1.3.2",
+  "org.spark-project.akka" %% "akka-actor"     % "2.3.4-spark"
+)
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-predictionio/blob/71266230/common/src/main/resources/application.conf
----------------------------------------------------------------------
diff --git a/common/src/main/resources/application.conf b/common/src/main/resources/application.conf
new file mode 100644
index 0000000..cccab96
--- /dev/null
+++ b/common/src/main/resources/application.conf
@@ -0,0 +1,11 @@
+akka {
+  log-config-on-start = false
+  loggers = ["akka.event.slf4j.Slf4jLogger"]
+  loglevel = "INFO"
+}
+
+spray.can {
+  server {
+    verbose-error-messages = "on"
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-predictionio/blob/71266230/common/src/main/scala/io/prediction/authentication/Authentication.scala
----------------------------------------------------------------------
diff --git a/common/src/main/scala/io/prediction/authentication/Authentication.scala b/common/src/main/scala/io/prediction/authentication/Authentication.scala
new file mode 100644
index 0000000..8b33175
--- /dev/null
+++ b/common/src/main/scala/io/prediction/authentication/Authentication.scala
@@ -0,0 +1,50 @@
+package io.prediction.authentication
+
+/**
+  * Created by ykhodorkovsky on 3/1/16.
+  */
+package io.prediction.configuration
+
+/**
+  * This is a (very) simple authentication for the dashboard and engine servers
+  * It is highly recommended to implement a stonger authentication mechanism
+  */
+
+import java.io.File
+
+import com.typesafe.config.ConfigFactory
+
+import scala.concurrent.ExecutionContext.Implicits.global
+import spray.http.HttpRequest
+import spray.routing.{AuthenticationFailedRejection, RequestContext}
+import spray.routing.authentication._
+import spray.routing.directives.AuthMagnet
+import scala.concurrent.Future
+
+
+trait KeyAuthentication {
+
+  object ServerKey {
+    val serverConfig = ConfigFactory.parseFile(new File("conf/server.conf"))
+
+    val key = serverConfig.getString("server.accessKey")
+    def get: String = key
+    def param: String = "accessKey"
+  }
+
+  def withAccessKeyFromFile: RequestContext => Future[Authentication[HttpRequest]] = {
+    ctx: RequestContext =>
+      val accessKeyParamOpt = ctx.request.uri.query.get(ServerKey.param)
+      Future {
+
+        val passedKey = accessKeyParamOpt.getOrElse {
+          Left(AuthenticationFailedRejection(
+            AuthenticationFailedRejection.CredentialsRejected, List()))
+        }
+
+        if (passedKey.equals(ServerKey.get)) Right(ctx.request)
+        else Left(AuthenticationFailedRejection(AuthenticationFailedRejection.CredentialsRejected, List()))
+
+      }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-predictionio/blob/71266230/common/src/main/scala/io/prediction/configuration/SSLConfiguration.scala
----------------------------------------------------------------------
diff --git a/common/src/main/scala/io/prediction/configuration/SSLConfiguration.scala b/common/src/main/scala/io/prediction/configuration/SSLConfiguration.scala
new file mode 100644
index 0000000..63ed2ab
--- /dev/null
+++ b/common/src/main/scala/io/prediction/configuration/SSLConfiguration.scala
@@ -0,0 +1,52 @@
+package io.prediction.configuration
+
+/**
+  * Created by ykhodorkovsky on 2/26/16.
+  */
+
+import java.io.FileInputStream
+import java.io.File
+import java.security.KeyStore
+import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory}
+
+import com.typesafe.config.ConfigFactory
+import spray.io.ServerSSLEngineProvider
+
+trait SSLConfiguration {
+
+  private val serverConfig = ConfigFactory.parseFile(new File("conf/server.conf"))
+
+  private val keyStoreResource = serverConfig.getString("server.ssl-keystore-resource")
+  private val password = serverConfig.getString("server.ssl-keystore-pass")
+  private val keyAlias = serverConfig.getString("server.ssl-key-alias")
+
+  private val keyStore = {
+
+    //Loading keystore from specified file
+    val clientStore = KeyStore.getInstance("JKS")
+    val inputStream = new FileInputStream(keyStoreResource)
+    clientStore.load(inputStream, password.toCharArray)
+    inputStream.close()
+    clientStore
+  }
+
+  //Creating SSL context
+  implicit def sslContext: SSLContext = {
+    val context = SSLContext.getInstance("TLS")
+    val tmf: TrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm)
+    val kmf: KeyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm)
+    kmf.init(keyStore, password.toCharArray)
+    tmf.init(keyStore)
+    context.init(kmf.getKeyManagers, tmf.getTrustManagers, null)
+    context
+  }
+
+  //provide implicit SSLEngine with some protocols
+  implicit def sslEngineProvider: ServerSSLEngineProvider = {
+    ServerSSLEngineProvider { engine =>
+      engine.setEnabledCipherSuites(Array("TLS_RSA_WITH_AES_256_CBC_SHA", "TLS_ECDH_ECDSA_WITH_RC4_128_SHA", "TLS_RSA_WITH_AES_128_CBC_SHA"))
+      engine.setEnabledProtocols(Array("TLSv1", "TLSv1.2", "TLSv1.1"))
+      engine
+    }
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-predictionio/blob/71266230/conf/keystore.jks
----------------------------------------------------------------------
diff --git a/conf/keystore.jks b/conf/keystore.jks
new file mode 100644
index 0000000..64ec17b
Binary files /dev/null and b/conf/keystore.jks differ

http://git-wip-us.apache.org/repos/asf/incubator-predictionio/blob/71266230/conf/server.conf
----------------------------------------------------------------------
diff --git a/conf/server.conf b/conf/server.conf
new file mode 100644
index 0000000..5a3bffe
--- /dev/null
+++ b/conf/server.conf
@@ -0,0 +1,9 @@
+# Engine and dashboard Server related configurations
+server {
+
+  accessKey = ""
+
+  ssl-keystore-resource = "conf/keystore.jks"
+  ssl-keystore-pass = "pioserver"
+  ssl-key-alias = "selfsigned"
+}

http://git-wip-us.apache.org/repos/asf/incubator-predictionio/blob/71266230/core/src/main/resources/application.conf
----------------------------------------------------------------------
diff --git a/core/src/main/resources/application.conf b/core/src/main/resources/application.conf
deleted file mode 100644
index 93dd8d4..0000000
--- a/core/src/main/resources/application.conf
+++ /dev/null
@@ -1,4 +0,0 @@
-akka {
-  loggers = ["akka.event.slf4j.Slf4jLogger"]
-  loglevel = "INFO"
-}

http://git-wip-us.apache.org/repos/asf/incubator-predictionio/blob/71266230/core/src/main/scala/io/prediction/workflow/CreateServer.scala
----------------------------------------------------------------------
diff --git a/core/src/main/scala/io/prediction/workflow/CreateServer.scala b/core/src/main/scala/io/prediction/workflow/CreateServer.scala
index 829c6b6..d5076ef 100644
--- a/core/src/main/scala/io/prediction/workflow/CreateServer.scala
+++ b/core/src/main/scala/io/prediction/workflow/CreateServer.scala
@@ -30,8 +30,11 @@ import com.twitter.bijection.Injection
 import com.twitter.chill.KryoBase
 import com.twitter.chill.KryoInjection
 import com.twitter.chill.ScalaKryoInstantiator
+import com.typesafe.config.ConfigFactory
 import de.javakaffee.kryoserializers.SynchronizedCollectionsSerializer
 import grizzled.slf4j.Logging
+import io.prediction.authentication.io.prediction.configuration.KeyAuthentication
+import io.prediction.configuration.SSLConfiguration
 import io.prediction.controller.Engine
 import io.prediction.controller.Params
 import io.prediction.controller.Utils
@@ -47,10 +50,12 @@ import org.json4s._
 import org.json4s.native.JsonMethods._
 import org.json4s.native.Serialization.write
 import spray.can.Http
+import spray.can.server.ServerSettings
 import spray.http.MediaTypes._
 import spray.http._
 import spray.httpx.Json4sSupport
 import spray.routing._
+import spray.routing.authentication.{UserPass, BasicAuth}
 
 import scala.concurrent.ExecutionContext.Implicits.global
 import scala.concurrent.Future
@@ -60,6 +65,7 @@ import scala.language.existentials
 import scala.util.Failure
 import scala.util.Random
 import scala.util.Success
+import scalaj.http.HttpOptions
 
 class KryoInstantiator(classLoader: ClassLoader) extends ScalaKryoInstantiator {
   override def newKryo(): KryoBase = {
@@ -103,6 +109,7 @@ case class StopServer()
 case class ReloadServer()
 case class UpgradeCheck()
 
+
 object CreateServer extends Logging {
   val actorSystem = ActorSystem("pio-server")
   val engineInstances = Storage.getMetaDataEngineInstances
@@ -274,11 +281,11 @@ class UpgradeActor(engineClass: String) extends Actor {
   }
 }
 
-class MasterActor(
+class MasterActor (
     sc: ServerConfig,
     engineInstance: EngineInstance,
     engineFactoryName: String,
-    manifest: EngineManifest) extends Actor {
+    manifest: EngineManifest) extends Actor with SSLConfiguration with KeyAuthentication {
   val log = Logging(context.system, this)
   implicit val system = context.system
   var sprayHttpListener: Option[ActorRef] = None
@@ -286,11 +293,11 @@ class MasterActor(
   var retry = 3
 
   def undeploy(ip: String, port: Int): Unit = {
-    val serverUrl = s"http://${ip}:${port}"
+    val serverUrl = s"https://${ip}:${port}"
     log.info(
       s"Undeploying any existing engine instance at $serverUrl")
     try {
-      val code = scalaj.http.Http(s"$serverUrl/stop").asString.code
+      val code = scalaj.http.Http(s"$serverUrl/stop").option(HttpOptions.allowUnsafeSSL).param(ServerKey.param, ServerKey.get).method("POST").asString.code
       code match {
         case 200 => Unit
         case 404 => log.error(
@@ -321,7 +328,8 @@ class MasterActor(
       self ! BindServer()
     case x: BindServer =>
       currentServerActor map { actor =>
-        IO(Http) ! Http.Bind(actor, interface = sc.ip, port = sc.port)
+        val settings = ServerSettings(system)
+        IO(Http) ! Http.Bind(actor, interface = sc.ip, port = sc.port, settings = Some(settings.copy(sslEncryption = true)))
       } getOrElse {
         log.error("Cannot bind a non-existing server backend.")
       }
@@ -345,7 +353,8 @@ class MasterActor(
         val actor = createServerActor(sc, lr, engineFactoryName, manifest)
         sprayHttpListener.map { l =>
           l ! Http.Unbind(5.seconds)
-          IO(Http) ! Http.Bind(actor, interface = sc.ip, port = sc.port)
+          val settings = ServerSettings(system)
+          IO(Http) ! Http.Bind(actor, interface = sc.ip, port = sc.port, settings = Some(settings.copy(sslEncryption = true)))
           currentServerActor.get ! Kill
           currentServerActor = Some(actor)
         } getOrElse {
@@ -357,7 +366,7 @@ class MasterActor(
           s"${manifest.version}. Abort reloading.")
       }
     case x: Http.Bound =>
-      val serverUrl = s"http://${sc.ip}:${sc.port}"
+      val serverUrl = s"https://${sc.ip}:${sc.port}"
       log.info(s"Engine is deployed and running. Engine API is live at ${serverUrl}.")
       sprayHttpListener = Some(sender)
     case x: Http.CommandFailed =>
@@ -411,7 +420,7 @@ class ServerActor[Q, P](
     val algorithmsParams: Seq[Params],
     val models: Seq[Any],
     val serving: BaseServing[Q, P],
-    val servingParams: Params) extends Actor with HttpService {
+    val servingParams: Params) extends Actor with HttpService with KeyAuthentication {
   val serverStartTime = DateTime.now
   val log = Logging(context.system, this)
 
@@ -458,6 +467,8 @@ class ServerActor[Q, P](
     writer.toString
   }
 
+  private def getUser(up :UserPass) : String = up.user
+
   val myRoute =
     path("") {
       get {
@@ -641,20 +652,24 @@ class ServerActor[Q, P](
       }
     } ~
     path("reload") {
-      get {
-        complete {
-          context.actorSelection("/user/master") ! ReloadServer()
-          "Reloading..."
+      authenticate(withAccessKeyFromFile) { request =>
+        post {
+          complete {
+            context.actorSelection("/user/master") ! ReloadServer()
+            "Reloading..."
+          }
         }
       }
     } ~
     path("stop") {
-      get {
-        complete {
-          context.system.scheduler.scheduleOnce(1.seconds) {
-            context.actorSelection("/user/master") ! StopServer()
+      authenticate(withAccessKeyFromFile) { request =>
+        post {
+          complete {
+            context.system.scheduler.scheduleOnce(1.seconds) {
+              context.actorSelection("/user/master") ! StopServer()
+            }
+            "Shutting down..."
           }
-          "Shutting down..."
         }
       }
     } ~

http://git-wip-us.apache.org/repos/asf/incubator-predictionio/blob/71266230/data/src/main/scala/io/prediction/data/api/EventServer.scala
----------------------------------------------------------------------
diff --git a/data/src/main/scala/io/prediction/data/api/EventServer.scala b/data/src/main/scala/io/prediction/data/api/EventServer.scala
index 0ebc279..0b8f358 100644
--- a/data/src/main/scala/io/prediction/data/api/EventServer.scala
+++ b/data/src/main/scala/io/prediction/data/api/EventServer.scala
@@ -33,6 +33,7 @@ import io.prediction.data.storage.EventJson4sSupport
 import io.prediction.data.storage.BatchEventsJson4sSupport
 import io.prediction.data.storage.LEvents
 import io.prediction.data.storage.Storage
+import io.prediction.configuration.SSLConfiguration
 import org.json4s.DefaultFormats
 import org.json4s.Formats
 import org.json4s.JObject
@@ -556,7 +557,7 @@ class EventServerActor(
     val eventClient: LEvents,
     val accessKeysClient: AccessKeys,
     val channelsClient: Channels,
-    val config: EventServerConfig) extends Actor {
+    val config: EventServerConfig) extends Actor with SSLConfiguration {
   val log = Logging(context.system, this)
   val child = context.actorOf(
     Props(classOf[EventServiceActor],

http://git-wip-us.apache.org/repos/asf/incubator-predictionio/blob/71266230/tools/src/main/scala/io/prediction/tools/dashboard/Dashboard.scala
----------------------------------------------------------------------
diff --git a/tools/src/main/scala/io/prediction/tools/dashboard/Dashboard.scala b/tools/src/main/scala/io/prediction/tools/dashboard/Dashboard.scala
index 30fdbd2..f79f310 100644
--- a/tools/src/main/scala/io/prediction/tools/dashboard/Dashboard.scala
+++ b/tools/src/main/scala/io/prediction/tools/dashboard/Dashboard.scala
@@ -15,8 +15,13 @@
 
 package io.prediction.tools.dashboard
 
+import com.typesafe.config.ConfigFactory
+import io.prediction.authentication.io.prediction.configuration.KeyAuthentication
+import io.prediction.configuration.SSLConfiguration
 import io.prediction.data.storage.Storage
-
+import spray.can.server.ServerSettings
+import spray.routing.directives.AuthMagnet
+import scala.concurrent.{Future, ExecutionContext}
 import akka.actor.{ActorContext, Actor, ActorSystem, Props}
 import akka.io.IO
 import akka.pattern.ask
@@ -27,6 +32,7 @@ import spray.can.Http
 import spray.http._
 import spray.http.MediaTypes._
 import spray.routing._
+import spray.routing.authentication.{Authentication, UserPass, BasicAuth}
 
 import scala.concurrent.duration._
 
@@ -34,7 +40,7 @@ case class DashboardConfig(
   ip: String = "localhost",
   port: Int = 9000)
 
-object Dashboard extends Logging {
+object Dashboard extends Logging with SSLConfiguration{
   def main(args: Array[String]): Unit = {
     val parser = new scopt.OptionParser[DashboardConfig]("Dashboard") {
       opt[String]("ip") action { (x, c) =>
@@ -55,7 +61,8 @@ object Dashboard extends Logging {
     val service =
       system.actorOf(Props(classOf[DashboardActor], dc), "dashboard")
     implicit val timeout = Timeout(5.seconds)
-    IO(Http) ? Http.Bind(service, interface = dc.ip, port = dc.port)
+    val settings = ServerSettings(system)
+    IO(Http) ? Http.Bind(service, interface = dc.ip, port = dc.port, settings = Some(settings.copy(sslEncryption = true)))
     system.awaitTermination
   }
 }
@@ -67,22 +74,26 @@ class DashboardActor(
   def receive: Actor.Receive = runRoute(dashboardRoute)
 }
 
-trait DashboardService extends HttpService with CORSSupport {
+trait DashboardService extends HttpService with KeyAuthentication with CORSSupport {
+
+  implicit def executionContext: ExecutionContext = actorRefFactory.dispatcher
   val dc: DashboardConfig
   val evaluationInstances = Storage.getMetaDataEvaluationInstances
   val pioEnvVars = sys.env.filter(kv => kv._1.startsWith("PIO_"))
   val serverStartTime = DateTime.now
   val dashboardRoute =
     path("") {
-      get {
-        respondWithMediaType(`text/html`) {
-          complete {
-            val completedInstances = evaluationInstances.getCompleted
-            html.index(
-              dc,
-              serverStartTime,
-              pioEnvVars,
-              completedInstances).toString
+      authenticate(withAccessKeyFromFile) { request =>
+        get {
+          respondWithMediaType(`text/html`) {
+            complete {
+              val completedInstances = evaluationInstances.getCompleted
+              html.index(
+                dc,
+                serverStartTime,
+                pioEnvVars,
+                completedInstances).toString
+            }
           }
         }
       }