diff --git a/.travis.yml b/.travis.yml index baf1501238..771f6522a6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,9 @@ language: - java - scala sudo: false +before_script: +- mkdir -p $HOME/.sbt/launchers/0.13.9/ +- curl -L -o $HOME/.sbt/launchers/0.13.9/sbt-launch.jar http://dl.bintray.com/typesafe/ivy-releases/org.scala-sbt/sbt-launch/0.13.9/sbt-launch.jar script: - echo "TRAVIS_PULL_REQUEST" $TRAVIS_PULL_REQUEST - echo "TRAVIS_BRANCH" $TRAVIS_BRANCH diff --git a/conf/gear.conf b/conf/gear.conf index f61346dfe0..dc4455d84b 100644 --- a/conf/gear.conf +++ b/conf/gear.conf @@ -259,4 +259,42 @@ gearpump { enabled = true } } + + ## Security related settings + security { + + ## Whether enable authentication for UI Server + ui-authentication-enabled = false + + ## What authenticator to use. The class must implement interface + ## io.gearpump.security.Authenticator + ui-authenticator = "io.gearpump.security.ConfigFileBasedAuthenticator" + + ## Configuration options for authenticator io.gearpump.security.ConfigFileBasedAuthenticator + config-file-based-authenticator = { + ## Format: username = "password_hash_value" + ## password_hash_value can be generated by running shell tool: + ## bin/gear io.gearpump.security.PasswordUtil -password + + ## Admin users have super permission to do everything + admins = { + ## Default Admin. Username: admin, password: admin + ## !!! Please replace this builtin account for production cluster for security reason. !!! + "admin" = "AeGxGOxlU8QENdOXejCeLxy+isrCv0TrS37HwA==" + } + + ## normal user have special permission for certain operations. + users = { + ## "user" = "AeGxGOxlU8QENdOXejCeLxy+isrCv0TrS37HwA==" + } + + ## Guest user can only view the UI with minimum permission. With no permission to submit/change/kill + ## a running application. + guests = { + ## Default guest. Username: guest, Password: guest + ## !!! Please replace this builtin account for production cluster for security reason. !!! + "guest" = "ws+2Dy/FHX4cBb3uKGTR64kZWlWbC91XZRRoew==" + } + } + } } diff --git a/core/src/main/resources/reference.conf b/core/src/main/resources/reference.conf index 5b12ab9348..354050a883 100644 --- a/core/src/main/resources/reference.conf +++ b/core/src/main/resources/reference.conf @@ -244,6 +244,44 @@ gearpump { single-thread-dispatcher { type = PinnedDispatcher } + + ## Security related settings + security { + + ## Whether enable authentication for UI Server + ui-authentication-enabled = false + + ## What authenticator to use. The class must implement interface + ## io.gearpump.security.Authenticator + ui-authenticator = "io.gearpump.security.ConfigFileBasedAuthenticator" + + ## Configuration options for authenticator io.gearpump.security.ConfigFileBasedAuthenticator + config-file-based-authenticator = { + ## Format: username = "password_hash_value" + ## password_hash_value can be generated by running shell tool: + ## bin/gear io.gearpump.security.PasswordUtil -password + + ## Admin users have super permission to do everything + admins = { + ## Default Admin. Username: admin, password: admin + ## !!! Please replace this builtin account for production cluster for security reason. !!! + "admin" = "AeGxGOxlU8QENdOXejCeLxy+isrCv0TrS37HwA==" + } + + ## normal user have special permission for certain operations. + users = { + ## "user" = "AeGxGOxlU8QENdOXejCeLxy+isrCv0TrS37HwA==" + } + + ## Guest user can only view the UI with minimum permission. With no permission to submit/change/kill + ## a running application. + guests = { + ## Default guest. Username: guest, Password: guest + ## !!! Please replace this builtin account for production cluster for security reason. !!! + "guest" = "ws+2Dy/FHX4cBb3uKGTR64kZWlWbC91XZRRoew==" + } + } + } } ### Akka system configuration for master nodes @@ -319,7 +357,6 @@ base { akka.scheduler.tick-duration = 1 akka { - http { client { parsing { @@ -327,11 +364,33 @@ base { } } server { + remote-address-header = on parsing { max-content-length = 2048m illegal-header-warnings = off } } + + ## Akka-http session related settings + session { + + serverSecret = "!!!please change this to a value only you know!!!" + + clientSession { + cookie { + name = "gearpump_token" + ## domain = "..." + path = "/" + ## maxAge = 0 + secure = false + httpOnly = true + } + + ## Session lifetime. + maxAgeSeconds = 3600 + encryptData = true + } + } } test { @@ -411,4 +470,4 @@ windows { ### On windows, the value must be larger than 10ms, check ### https://github.com/akka/akka/blob/master/akka-actor/src/main/scala/akka/actor/Scheduler.scala#L204 akka.scheduler.tick-duration = 10 -} +} \ No newline at end of file diff --git a/core/src/main/scala/io/gearpump/security/Authenticator.scala b/core/src/main/scala/io/gearpump/security/Authenticator.scala new file mode 100644 index 0000000000..afb13a93d4 --- /dev/null +++ b/core/src/main/scala/io/gearpump/security/Authenticator.scala @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package io.gearpump.security +import io.gearpump.security.Authenticator.AuthenticationResult + +import scala.concurrent.{ExecutionContext, Future} + + +/** + * Authenticator for UI dashboard. + * + * Sub Class must implement a constructor with signature like this: + * this(config: Config) + * + */ +trait Authenticator { + + // TODO: Change the signature to return more attributes of user + // credentials... + def authenticate(user: String, password: String, ec: ExecutionContext): Future[AuthenticationResult] +} + +object Authenticator { + + trait AuthenticationResult { + + def authenticated: Boolean + + def permissionLevel: Int + } + + val UnAuthenticated = new AuthenticationResult{ + override val authenticated = false + override val permissionLevel = -1 + } + + val Guest = new AuthenticationResult{ + override val authenticated = true + override val permissionLevel = 1000 + } + + val User = new AuthenticationResult{ + override val authenticated = true + override val permissionLevel = 1000 + Guest.permissionLevel + } + + val Admin = new AuthenticationResult{ + override val authenticated = true + override val permissionLevel = 1000 + User.permissionLevel + } +} \ No newline at end of file diff --git a/core/src/main/scala/io/gearpump/security/ConfigFileBasedAuthenticator.scala b/core/src/main/scala/io/gearpump/security/ConfigFileBasedAuthenticator.scala new file mode 100644 index 0000000000..204c9f022f --- /dev/null +++ b/core/src/main/scala/io/gearpump/security/ConfigFileBasedAuthenticator.scala @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package io.gearpump.security + +import io.gearpump.security.Authenticator.AuthenticationResult +import io.gearpump.security.ConfigFileBasedAuthenticator._ +import com.typesafe.config.Config +import scala.concurrent.{ExecutionContext, Future} + +object ConfigFileBasedAuthenticator { + + private val ROOT = "gearpump.security.config-file-based-authenticator" + private val ADMINS = ROOT + "." + "admins" + private val USERS = ROOT + "." + "users" + private val GUESTS = ROOT + "." + "guests" + + private case class Credentials(admins: Map[String, String], users: Map[String, String], guests: Map[String, String]) { + def verify(user: String, password: String): AuthenticationResult = { + if (admins.contains(user)) { + if (verify(user, password, admins)) { + Authenticator.Admin + } else { + Authenticator.UnAuthenticated + } + } else if (users.contains(user)) { + if (verify(user, password, users)) { + Authenticator.User + } else { + Authenticator.UnAuthenticated + } + } else if (guests.contains(user)) { + if (verify(user, password, guests)) { + Authenticator.Guest + } else { + Authenticator.UnAuthenticated + } + } else { + Authenticator.UnAuthenticated + } + } + + private def verify(user: String, password: String, map: Map[String, String]): Boolean = { + val storedPass = map(user) + PasswordUtil.verify(password, storedPass) + } + } +} + +/** + * UI dashboard authenticator based on configuration file. + * + * It has three categories of users: admins, users, and guests. + * admins have unlimited permission, like shutdown a cluster, add/remove machines. + * users have limited permission to submit an application and etc.. + * guests can not submit/kill applications, but can view the application status. + * + * see conf/gear.conf section gearpump.security.config-file-based-authenticator to find information + * about how to configure this authenticator. + * + * [Security consideration] + * It will keep one-way sha1 digest of password instead of password itself. The original password is NOT + * kept in any way, so generally it is safe. + * + * digesting flow (from original password to digest): + * random salt byte array of length 8 -> byte array of (salt + sha1(salt, password)) -> base64Encode + * + * verification user input password with stored digest: + * base64Decode -> extract salt -> do sha1(salt, password) -> generate digest: salt + sha1 -> + * compare the generated digest with the stored digest. + * + */ +class ConfigFileBasedAuthenticator(config: Config) extends Authenticator { + + private val credentials = loadCredentials(config) + + override def authenticate(user: String, password: String, ec: ExecutionContext): Future[AuthenticationResult] = { + implicit val ctx = ec + Future { + credentials.verify(user, password) + } + } + + private def loadCredentials(config: Config): Credentials = { + val admins = configToMap(config, ADMINS) + val users = configToMap(config, USERS) + val guests = configToMap(config, GUESTS) + new Credentials(admins, users, guests) + } + + private def configToMap(config : Config, path: String) = { + import scala.collection.JavaConverters._ + config.getConfig(path).root.unwrapped.asScala.toMap map { case (k, v) => k -> v.toString } + } +} \ No newline at end of file diff --git a/core/src/main/scala/io/gearpump/security/PasswordUtil.scala b/core/src/main/scala/io/gearpump/security/PasswordUtil.scala new file mode 100644 index 0000000000..f8eafddc69 --- /dev/null +++ b/core/src/main/scala/io/gearpump/security/PasswordUtil.scala @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package io.gearpump.security + +import java.security.MessageDigest +import sun.misc.{BASE64Decoder, BASE64Encoder} +import scala.util.Try + +/** + * Util to verify whether user input password is valid or not. + * It use sha1 to do the digesting. + */ +object PasswordUtil { + private val SALT_LENGTH = 8 + + /** + * verification user input password with stored digest: + * base64Decode -> extract salt -> do sha1(salt, password) -> + * generate digest: salt + sha1 -> compare the generated digest with the stored digest. + */ + def verify(password: String, stored: String): Boolean = { + Try { + val decoded = base64Decode(stored) + val salt = new Array[Byte](SALT_LENGTH) + Array.copy(decoded, 0, salt, 0, SALT_LENGTH) + + hash(password, salt) == stored + }.getOrElse(false) + } + /** + * digesting flow (from original password to digest): + * random salt byte array of length 8 -> byte array of (salt + sha1(salt, password)) -> base64Encode + */ + def hash(password: String): String = { + // Salt generation 64 bits long + val salt = new Array[Byte](SALT_LENGTH) + new java.util.Random().nextBytes(salt) + hash(password, salt) + } + + private def hash(password: String, salt: Array[Byte]): String = { + val digest = MessageDigest.getInstance("SHA-1") + digest.reset() + digest.update(salt) + var input = digest.digest(password.getBytes("UTF-8")) + digest.reset() + input = digest.digest(input) + val withSalt = salt ++ input + base64Encode(withSalt) + } + + private def base64Encode(data: Array[Byte]): String = { + val endecoder = new BASE64Encoder() + endecoder.encode(data) + } + + private def base64Decode(data: String): Array[Byte] = { + val decoder = new BASE64Decoder() + decoder.decodeBuffer(data) + } + + private def help = { + Console.println("usage: gear io.gearpump.security.PasswordUtil -password ") + } + + def main(args: Array[String]): Unit = { + if (args.length != 2 || args(0) != "-password") { + help + } else { + val pass = args(1) + val result = hash(pass) + Console.println("Here is the hashed password") + Console.println("==============================") + Console.println(result) + } + } +} \ No newline at end of file diff --git a/core/src/main/scala/io/gearpump/util/Constants.scala b/core/src/main/scala/io/gearpump/util/Constants.scala index a800f87561..98efc1d534 100644 --- a/core/src/main/scala/io/gearpump/util/Constants.scala +++ b/core/src/main/scala/io/gearpump/util/Constants.scala @@ -140,4 +140,7 @@ object Constants { val GEARPUMP_METRICS_MAX_LIMIT = "gearpump.metrics.akka.max-limit-on-query" val GEARPUMP_METRICS_AGGREGATORS = "gearpump.metrics.akka.metrics-aggregator-class" + + val GEARPUMP_UI_SECURITY_ENABLED = "gearpump.security.ui-authentication-enabled" + val GEARPUMP_UI_AUTHENTICATOR_CLASS = "gearpump.security.ui-authenticator" } diff --git a/core/src/test/scala/io/gearpump/security/ConfigFileBasedAuthenticatorSpec.scala b/core/src/test/scala/io/gearpump/security/ConfigFileBasedAuthenticatorSpec.scala new file mode 100644 index 0000000000..cde8cf50de --- /dev/null +++ b/core/src/test/scala/io/gearpump/security/ConfigFileBasedAuthenticatorSpec.scala @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package io.gearpump.security + +import akka.actor.ActorSystem +import io.gearpump.cluster.TestUtil +import io.gearpump.security.Authenticator.AuthenticationResult +import org.scalatest.{Matchers, FlatSpec} + +import scala.concurrent.Await +import scala.concurrent.duration._ + +class ConfigFileBasedAuthenticatorSpec extends FlatSpec with Matchers { + it should "authenticate correctly" in { + val config = TestUtil.DEFAULT_CONFIG + implicit val system = ActorSystem("ConfigFileBasedAuthenticatorSpec", config) + implicit val ec = system.dispatcher + val timeout = 30 seconds + + val authenticator = new ConfigFileBasedAuthenticator(config) + val guest = Await.result(authenticator.authenticate("guest", "guest", ec), timeout) + val admin = Await.result(authenticator.authenticate("admin", "admin", ec), timeout) + + val nonexist = Await.result(authenticator.authenticate("nonexist", "nonexist", ec), timeout) + + val failedGuest = Await.result(authenticator.authenticate("guest", "wrong", ec), timeout) + val failedAdmin = Await.result(authenticator.authenticate("admin", "wrong", ec), timeout) + + assert(guest == Authenticator.Guest) + assert(admin == Authenticator.Admin) + assert(nonexist == Authenticator.UnAuthenticated) + assert(failedGuest == Authenticator.UnAuthenticated) + assert(failedAdmin == Authenticator.UnAuthenticated) + + system.shutdown() + system.awaitTermination() + } +} diff --git a/core/src/test/scala/io/gearpump/security/PasswordUtilSpec.scala b/core/src/test/scala/io/gearpump/security/PasswordUtilSpec.scala new file mode 100644 index 0000000000..69abb80fb9 --- /dev/null +++ b/core/src/test/scala/io/gearpump/security/PasswordUtilSpec.scala @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package io.gearpump.security + +import org.scalatest.{Matchers, FlatSpec} + +class PasswordUtilSpec extends FlatSpec with Matchers { + + it should "verify the credential correctly" in { + val password = "password" + + val digest1 = PasswordUtil.hash(password) + val digest2 = PasswordUtil.hash(password) + + // we will use different salt each time, thus + // creating different hash. + assert(digest1 != digest2) + + // both are valid hash. + assert(PasswordUtil.verify(password, digest1)) + assert(PasswordUtil.verify(password, digest2)) + } +} diff --git a/docs/dev-ide-setup.md b/docs/dev-ide-setup.md index b82c7b630c..fed97e9845 100644 --- a/docs/dev-ide-setup.md +++ b/docs/dev-ide-setup.md @@ -8,6 +8,15 @@ title: IDE Preparation for Gearpump Development 2. Open menu "File->Open" to open Gearpump root project, then choose the Gearpump source folder. 3. All set. +**NOTE:** Intellij Scala plugin is already bundled with sbt. If you have Scala plugin installed, please don't install additional sbt plugin. Check your settings at "Settings -> Plugins" +**NOTE:** If you are behind a proxy, to speed up the build, please set the proxy for sbt in "Settings -> Build Tools > SBT". in input field "VM parameters", add +``` +-Dhttp.proxyHost= +-Dhttp.proxyPort= +-Dhttps.proxyHost= +-Dhttps.proxyPort= +``` + ### Eclipse IDE Setup I will show how to do this in eclipse LUNA. diff --git a/experiments/akkastream/src/test/scala/akka/stream/gearpump/AttributesSpec.scala b/experiments/akkastream/src/test/scala/akka/stream/gearpump/AttributesSpec.scala index 975c343a34..97a52bf196 100644 --- a/experiments/akkastream/src/test/scala/akka/stream/gearpump/AttributesSpec.scala +++ b/experiments/akkastream/src/test/scala/akka/stream/gearpump/AttributesSpec.scala @@ -1,11 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + package akka.stream.gearpump import akka.stream.Attributes import org.scalatest.{FlatSpec, Matchers, WordSpec} -/** - * Created by xzhong10 on 2015/11/6. - */ class AttributesSpec extends FlatSpec with Matchers { it should "merge the attributes together" in { val a = Attributes.name("aa") @@ -13,7 +28,7 @@ class AttributesSpec extends FlatSpec with Matchers { val c = a and b - println("DD:" + c.nameOrDefault()) + assert("aa-bb" == c.nameOrDefault()) } } diff --git a/project/Build.scala b/project/Build.scala index 30c40ba1b0..02bbbebb48 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -270,6 +270,7 @@ object Build extends sbt.Build { "com.typesafe.akka" %% "akka-http-testkit-experimental"% "1.0" % "test", "org.scalatest" %% "scalatest" % scalaTestVersion % "test", "com.lihaoyi" %% "upickle" % upickleVersion, + "com.softwaremill" %% "akka-http-session" % "0.1.4", "org.webjars" % "angularjs" % "1.4.8", "org.webjars" % "angular-ui-router" % "0.2.15", "org.webjars" % "bootstrap" % "3.3.6", @@ -277,6 +278,8 @@ object Build extends sbt.Build { "org.webjars" % "momentjs" % "2.10.6", "org.webjars" % "lodash" % "3.10.1", "org.webjars" % "font-awesome" % "4.5.0", + "org.webjars" % "jquery" % "2.2.0", + "org.webjars" % "jquery-cookie" % "1.4.1", "org.webjars.bower" % "angular-loading-bar" % "0.8.0", "org.webjars.bower" % "angular-smart-table" % "2.1.6", "org.webjars.bower" % "angular-motion" % "0.4.3", diff --git a/project/build.properties b/project/build.properties index 4b29a5b2a5..a4820ce974 100644 --- a/project/build.properties +++ b/project/build.properties @@ -16,4 +16,4 @@ # limitations under the License. # -sbt.version=0.13.7 +sbt.version=0.13.9 diff --git a/project/plugins.sbt b/project/plugins.sbt index 78c8bd038e..58b0aa6e82 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,7 +1,5 @@ resolvers += Resolver.url("fvunicorn", url("http://dl.bintray.com/fvunicorn/sbt-plugins"))(Resolver.ivyStylePatterns) -resolvers += Resolver.url("sbt-plugin", url("http://dl.bintray.com/sbt/sbt-plugin-releases"))(Resolver.ivyStylePatterns) - resolvers += Classpaths.sbtPluginReleases addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.4") diff --git a/services/dashboard/dashboard.js b/services/dashboard/dashboard.js index 559e6b6b84..b41dc225cf 100644 --- a/services/dashboard/dashboard.js +++ b/services/dashboard/dashboard.js @@ -6,6 +6,7 @@ angular.module('dashboard', [ 'ngAnimate', 'ngSanitize', + 'ngCookies', 'mgcrea.ngStrap', 'ui.router', 'ui.select', @@ -14,7 +15,6 @@ angular.module('dashboard', [ 'dashing', 'io.gearpump.models' ]) - // configure routes .config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) { @@ -67,6 +67,7 @@ angular.module('dashboard', [ restapiRoot: location.origin + location.pathname, restapiQueryInterval: 3 * 1000, // in milliseconds restapiQueryTimeout: 30 * 1000, // in milliseconds - restapiTaskLevelMetricsQueryLimit: 100 + restapiTaskLevelMetricsQueryLimit: 100, + loginUrl: location.origin + location.pathname + 'login/login.html' }) ; \ No newline at end of file diff --git a/services/dashboard/index.html b/services/dashboard/index.html index dfb369bb65..df94f0c037 100644 --- a/services/dashboard/index.html +++ b/services/dashboard/index.html @@ -43,7 +43,10 @@ + + + @@ -72,6 +75,7 @@ + diff --git a/services/dashboard/login/login.html b/services/dashboard/login/login.html new file mode 100644 index 0000000000..fb39aea33d --- /dev/null +++ b/services/dashboard/login/login.html @@ -0,0 +1,111 @@ + + + + + + + + + + Login + + + + + + Login + + + + +

+

Sign in to Gearpump

+ + + + + + +
+
+
+ + +
+
+
+ + + + + + \ No newline at end of file diff --git a/services/dashboard/login/login.js b/services/dashboard/login/login.js new file mode 100644 index 0000000000..e33c5accbd --- /dev/null +++ b/services/dashboard/login/login.js @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 + * See accompanying LICENSE file. + */ + +/** + * call rest service /login to setup login session tokens. + * If login succeeds, it will redirect to dashboard home page. + */ +function login() { + + var loginUrl = $("#loginUrl").attr('href'); + var index = $("#index").attr('href'); + + $.post(loginUrl, $("#loginForm").serialize() ).done( + function(msg) { + var user = $.parseJSON(msg); + $.cookie("username", user.user, { expires: 365, path: '/' }); + // clear the errors + $("#error").text(""); + // redirect to index.html + $(location).attr('href', index); + } + ) + .fail( function(xhr, textStatus, errorThrown) { + $("#error").text(textStatus + "(" + xhr.status + "): " + xhr.responseText); + }); +} + +/** + * call rest service /logout to clear the session tokens. + */ +function logout() { + var logoutUrl = $("#logoutUrl").attr('href'); + $.post(logoutUrl) +} + +$(document).ready(function() { + // Send a initial logout to clear the sessions. + logout(); +}); \ No newline at end of file diff --git a/services/dashboard/services/login_check.js b/services/dashboard/services/login_check.js new file mode 100644 index 0000000000..1d15dc4f22 --- /dev/null +++ b/services/dashboard/services/login_check.js @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 + * See accompanying LICENSE file. + */ + +angular.module('dashboard') +// Authentication Angular interceptor for http methods. +// If server respond 401 (unauthenticated), will redirect to login page. +.factory('authInterceptor', ['$q', 'conf', function ($q, conf) { + + // Defer the error response to caller after this timeout to avoid browser hang issue + // See https://github.com/gearpump/gearpump/issues/1855 + var deferErrorResponseMs = 3000; + return { + 'responseError': function(response) { + if (response.status == 401) { + window.location.href = conf.loginUrl; + } + + var deferred = $q.defer(); + setTimeout(function() { + deferred.reject(response); + }, 3000); + return deferred.promise; + + } + }; +}]) +.config(['$httpProvider', function ($httpProvider) { + $httpProvider.interceptors.push('authInterceptor'); +}]); \ No newline at end of file diff --git a/services/dashboard/services/streamingservice.js b/services/dashboard/services/streamingservice.js index 05b6b36654..b2352edb46 100644 --- a/services/dashboard/services/streamingservice.js +++ b/services/dashboard/services/streamingservice.js @@ -9,7 +9,7 @@ angular.module('dashboard.streamingservice', []) .factory('StreamingService', ['$http', '$timeout', 'conf', function ($http, $timeout, conf) { return { subscribe: function (request, scope, onMessage) { - $http.get(conf.restapiRoot + '/websocket/url') + $http.get(conf.restapiRoot + 'websocket/url') .success(function (data) { var ws = new WebSocket(data.url); ws.onmessage = onMessage; diff --git a/services/dashboard/views/landing/header.html b/services/dashboard/views/landing/header.html index fc67135aa9..9d2d037f05 100644 --- a/services/dashboard/views/landing/header.html +++ b/services/dashboard/views/landing/header.html @@ -22,6 +22,21 @@ Applications + + + + +