Skip to content

Commit

Permalink
fix gearpump#1247, add authentication for UI
Browse files Browse the repository at this point in the history
  • Loading branch information
clockfly committed Jan 13, 2016
1 parent ba3037a commit 5209f3a
Show file tree
Hide file tree
Showing 34 changed files with 1,218 additions and 360 deletions.
3 changes: 3 additions & 0 deletions .travis.yml
Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions conf/gear.conf
Expand Up @@ -259,4 +259,39 @@ gearpump {
enabled = true
}
}

## Security related settings
security {

## Whether enable authentication for UI Server
ui-authentication-enabled = true

## 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 = {
## Admin users have permission to submit/change/kill a running application.
administrators = {
## Default Admin. Username: admin, password: admin
## Format: username = "password_hash_value"
## password_hash_value can be generated by running shell tool:
## bin/gear io.gearpump.security.PasswordUtil -password <your raw password>
## !!! Please replace this builtin account for production cluster for security reason. !!!
"admin" = "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
## Format: username = "password_hash_value"
## password_hash_value can be generated by running shell tool:
## bin/gear io.gearpump.security.PasswordUtil -password <your raw password>
## !!! Please replace this builtin account for production cluster for security reason. !!!
"guest" = "ws+2Dy/FHX4cBb3uKGTR64kZWlWbC91XZRRoew=="
}
}
}
}
54 changes: 52 additions & 2 deletions core/src/main/resources/reference.conf
Expand Up @@ -244,6 +244,38 @@ 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 = {
## Admin users have permission to submit/change/kill a running application.
administrators = {
## Default Admin. Username: admin, password: admin
## !!! Please remove this builtin account for production cluster for security reason. !!!
## Format: username = "password_hash_value"
## password_hash_value can be generated by running
## gear io.gearpump.security.PasswordUtil -password <your raw password>
"admin" = "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 remove this builtin account for production cluster for security reason. !!!
"guest" = "ws+2Dy/FHX4cBb3uKGTR64kZWlWbC91XZRRoew=="
}
}
}
}

### Akka system configuration for master nodes
Expand Down Expand Up @@ -319,19 +351,37 @@ base {
akka.scheduler.tick-duration = 1

akka {

http {
client {
parsing {
max-content-length = 2048m
}
}
server {
remote-address-header = on
parsing {
max-content-length = 2048m
illegal-header-warnings = off
}
}

## Akka-http session related settings
session {
clientSession {
cookie {
name = "gearpump_token"
## domain = "..."
path = "/"
## maxAge = 0
secure = false
httpOnly = true
}

## Session lifetime.
maxAgeSeconds = 3600
encryptData = true
}
}
}

test {
Expand Down Expand Up @@ -411,4 +461,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
}
}
45 changes: 45 additions & 0 deletions core/src/main/scala/io/gearpump/security/Authenticator.scala
@@ -0,0 +1,45 @@
/*
* 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 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]
}

trait AuthenticationResult {

// Whether current user is a valid user.
def authenticated: Boolean

// Admin user have unlimited permission
def isAdministrator: Boolean
}
@@ -0,0 +1,94 @@
/*
* 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.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 + "." + "administrators"
private val GUESTS = ROOT + "." + "guests"

private class Result(override val authenticated: Boolean, override val isAdministrator: Boolean)
extends AuthenticationResult

private case class Credentials(admins: Map[String, String], guests: Map[String, String]) {
def verify(user: String, password: String): AuthenticationResult = {
if (admins.contains(user)) {
new Result(verify(user, password, admins), isAdministrator = true)
} else if (guests.contains(user)) {
new Result(verify(user, password, guests), isAdministrator = false)
} else {
new Result(authenticated = false, isAdministrator = false)
}
}

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 two categories of users: administrators, and guests.
* administrators have unlimited permission on the UI, while guests can not submit/kill applications.
*
* 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 guests = configToMap(config, GUESTS)
new Credentials(admins, 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 }
}
}
93 changes: 93 additions & 0 deletions 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 <your 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)
}
}
}
3 changes: 3 additions & 0 deletions core/src/main/scala/io/gearpump/util/Constants.scala
Expand Up @@ -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"
}
9 changes: 9 additions & 0 deletions docs/dev-ide-setup.md
Expand Up @@ -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=<proxy host>
-Dhttp.proxyPort=<port like 911>
-Dhttps.proxyHost=<proxy host>
-Dhttps.proxyPort=<port like 911>
```

### Eclipse IDE Setup

I will show how to do this in eclipse LUNA.
Expand Down

0 comments on commit 5209f3a

Please sign in to comment.