Skip to content

Commit

Permalink
#263 Add separate authSrv for authentication by key. Add removeKey AP…
Browse files Browse the repository at this point in the history
…I. Add hasKey to user json output

Enable authentication by key in module initialization (can't be disabled)
  • Loading branch information
To-om committed Sep 6, 2017
1 parent dbaff4e commit deb0e2c
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 43 deletions.
5 changes: 5 additions & 0 deletions thehive-backend/app/controllers/UserCtrl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ class UserCtrl @Inject() (
authSrv.getKey(id).map(Ok(_))
}

@Timed
def removeKey(id: String): Action[AnyContent] = authenticated(Roles.admin).async { implicit request
authSrv.removeKey(id).map(_ Ok)
}

@Timed
def renewKey(id: String): Action[AnyContent] = authenticated(Roles.admin).async { implicit request
authSrv.renewKey(id).map(Ok(_))
Expand Down
21 changes: 6 additions & 15 deletions thehive-backend/app/global/TheHive.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package global

import scala.collection.JavaConverters._

import play.api.libs.concurrent.AkkaGuiceSupport
import play.api.mvc.EssentialFilter
import play.api.{ Configuration, Environment, Logger, Mode }
import play.api.libs.concurrent.AkkaGuiceSupport

import com.google.inject.AbstractModule
import com.google.inject.name.Names
Expand All @@ -19,7 +19,7 @@ import services._

import org.elastic4play.models.BaseModelDef
import org.elastic4play.services.auth.MultiAuthSrv
import org.elastic4play.services.{ AuthSrv, AuthSrvFactory, MigrationOperations, TempFilter }
import org.elastic4play.services.{ AuthSrv, MigrationOperations, TempFilter }

class TheHive(
environment: Environment,
Expand All @@ -33,7 +33,6 @@ class TheHive(
val modelBindings = ScalaMultibinder.newSetBinder[BaseModelDef](binder)
val auditedModelBindings = ScalaMultibinder.newSetBinder[AuditedModel](binder)
val authBindings = ScalaMultibinder.newSetBinder[AuthSrv](binder)
val authFactoryBindings = ScalaMultibinder.newSetBinder[AuthSrvFactory](binder)

val reflectionClasses = new Reflections(new ConfigurationBuilder()
.forPackages("org.elastic4play")
Expand Down Expand Up @@ -61,17 +60,9 @@ class TheHive(
.getSubTypesOf(classOf[AuthSrv])
.asScala
.filterNot(c java.lang.reflect.Modifier.isAbstract(c.getModifiers) || c.isMemberClass)
.filterNot(_ == classOf[MultiAuthSrv])
.foreach { modelClass
authBindings.addBinding.to(modelClass)
}

reflectionClasses
.getSubTypesOf(classOf[AuthSrvFactory])
.asScala
.filterNot(c java.lang.reflect.Modifier.isAbstract(c.getModifiers))
.foreach { modelClass
authFactoryBindings.addBinding.to(modelClass)
.filterNot(c c == classOf[MultiAuthSrv] || c == classOf[TheHiveAuthSrv])
.foreach { authSrvClass
authBindings.addBinding.to(authSrvClass)
}

val filterBindings = ScalaMultibinder.newSetBinder[EssentialFilter](binder)
Expand All @@ -80,7 +71,7 @@ class TheHive(
filterBindings.addBinding.to[CSRFFilter]

bind[MigrationOperations].to[Migration]
bind[AuthSrv].to[MultiAuthSrv]
bind[AuthSrv].to[TheHiveAuthSrv]

bindActor[AuditActor]("AuditActor")
bindActor[DeadLetterMonitoringActor]("DeadLetterMonitoringActor")
Expand Down
6 changes: 4 additions & 2 deletions thehive-backend/app/models/User.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package models
import scala.concurrent.Future

import play.api.libs.json.JsValue.jsValueToJsLookup
import play.api.libs.json.{ JsArray, JsObject, JsString }
import play.api.libs.json.{ JsArray, JsBoolean, JsObject, JsString }

import models.JsonFormat.userStatusFormat
import services.AuditedModel
Expand Down Expand Up @@ -40,5 +40,7 @@ class User(model: UserModel, attributes: JsObject) extends EntityDef[UserModel,
override def getUserName = userName()
override def getRoles = roles()

override def toJson: JsObject = super.toJson + ("roles" JsArray(roles().map(r JsString(r.name.toLowerCase()))))
override def toJson: JsObject = super.toJson +
("roles" JsArray(roles().map(r JsString(r.name.toLowerCase())))) +
("hasKey" JsBoolean(key().isDefined))
}
59 changes: 59 additions & 0 deletions thehive-backend/app/services/KeyAuthSrv.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package services

import java.util.Base64
import javax.inject.{ Inject, Singleton }

import scala.concurrent.{ ExecutionContext, Future }
import scala.util.Random

import play.api.libs.json.JsArray
import play.api.mvc.RequestHeader

import akka.stream.Materializer
import akka.stream.scaladsl.Sink

import org.elastic4play.controllers.Fields
import org.elastic4play.services.{ AuthCapability, AuthContext, AuthSrv }
import org.elastic4play.{ AuthenticationError, BadRequestError }

@Singleton
class KeyAuthSrv @Inject() (
userSrv: UserSrv,
implicit val ec: ExecutionContext,
implicit val mat: Materializer) extends AuthSrv {
override val name = "key"

protected final def generateKey(): String = {
val bytes = Array.ofDim[Byte](24)
Random.nextBytes(bytes)
Base64.getEncoder.encodeToString(bytes)
}

override val capabilities = Set(AuthCapability.authByKey)

override def authenticate(key: String)(implicit request: RequestHeader): Future[AuthContext] = {
import org.elastic4play.services.QueryDSL._
// key attribute is sensitive so it is not possible to search on that field
userSrv.find("status" ~= "Ok", Some("all"), Nil)
._1
.filter(_.key().contains(key))
.runWith(Sink.headOption)
.flatMap {
case Some(user) userSrv.getFromUser(request, user)
case None Future.failed(AuthenticationError("Authentication failure"))
}
}

override def renewKey(username: String)(implicit authContext: AuthContext): Future[String] = {
val newKey = generateKey()
userSrv.update(username, Fields.empty.set("key", newKey)).map(_ newKey)
}

override def getKey(username: String)(implicit authContext: AuthContext): Future[String] = {
userSrv.get(username).map(_.key().getOrElse(throw BadRequestError(s"User $username hasn't key")))
}

override def removeKey(username: String)(implicit authContext: AuthContext): Future[Unit] = {
userSrv.update(username, Fields.empty.set("key", JsArray())).map(_ ())
}
}
28 changes: 2 additions & 26 deletions thehive-backend/app/services/LocalAuthSrv.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ import scala.util.Random
import play.api.mvc.RequestHeader

import akka.stream.Materializer
import akka.stream.scaladsl.Sink
import models.User

import org.elastic4play.controllers.Fields
import org.elastic4play.services.{ AuthCapability, AuthContext, AuthSrv }
import org.elastic4play.utils.Hasher
import org.elastic4play.{ AuthenticationError, AuthorizationError, BadRequestError }
import org.elastic4play.{ AuthenticationError, AuthorizationError }

@Singleton
class LocalAuthSrv @Inject() (
Expand All @@ -23,7 +22,7 @@ class LocalAuthSrv @Inject() (
implicit val mat: Materializer) extends AuthSrv {

val name = "local"
override val capabilities = Set(AuthCapability.changePassword, AuthCapability.setPassword, AuthCapability.renewKey)
override val capabilities = Set(AuthCapability.changePassword, AuthCapability.setPassword)

private[services] def doAuthenticate(user: User, password: String): Boolean = {
user.password().map(_.split(",", 2)).fold(false) {
Expand All @@ -41,19 +40,6 @@ class LocalAuthSrv @Inject() (
}
}

override def authenticate(key: String)(implicit request: RequestHeader): Future[AuthContext] = {
import org.elastic4play.services.QueryDSL._
// key attribute is sensitive so it is not possible to search on that field
userSrv.find("status" ~= "Ok", Some("all"), Nil)
._1
.filter(_.key().contains(key))
.runWith(Sink.headOption)
.flatMap {
case Some(user) userSrv.getFromUser(request, user)
case None Future.failed(AuthenticationError("Authentication failure"))
}
}

override def changePassword(username: String, oldPassword: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = {
userSrv.get(username).flatMap { user
if (doAuthenticate(user, oldPassword)) setPassword(username, newPassword)
Expand All @@ -66,14 +52,4 @@ class LocalAuthSrv @Inject() (
val newHash = seed + "," + Hasher("SHA-256").fromString(seed + newPassword).head.toString
userSrv.update(username, Fields.empty.set("password", newHash)).map(_ ())
}

override def renewKey(username: String)(implicit authContext: AuthContext): Future[String] = {
val newKey = generateKey()
userSrv.update(username, Fields.empty.set("key", newKey)).map(_ newKey)
}

override def getKey(username: String)(implicit authContext: AuthContext): Future[String] = {
userSrv.get(username).map(_.key().getOrElse(throw BadRequestError(s"User $username hasn't key")))
}

}
50 changes: 50 additions & 0 deletions thehive-backend/app/services/TheHiveAuthSrv.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package services

import javax.inject.{ Inject, Singleton }

import scala.collection.immutable
import scala.concurrent.{ ExecutionContext, Future }
import scala.util.{ Failure, Success }

import play.api.mvc.RequestHeader
import play.api.{ Configuration, Logger }

import org.elastic4play.AuthenticationError
import org.elastic4play.services.{ AuthContext, AuthSrv }
import org.elastic4play.services.auth.MultiAuthSrv

object TheHiveAuthSrv {
private[TheHiveAuthSrv] lazy val logger = Logger(getClass)

def getAuthSrv(authTypes: Seq[String], authModules: immutable.Set[AuthSrv]): Seq[AuthSrv] = {
("key" +: authTypes.filterNot(_ == "key"))
.flatMap { authType
authModules.find(_.name == authType)
.orElse {
logger.error(s"Authentication module $authType not found")
None
}
}
}
}

@Singleton
class TheHiveAuthSrv @Inject() (
configuration: Configuration,
authModules: immutable.Set[AuthSrv],
userSrv: UserSrv,
override implicit val ec: ExecutionContext) extends MultiAuthSrv(
TheHiveAuthSrv.getAuthSrv(
configuration.getOptional[Seq[String]]("auth.type").getOrElse(Seq("local")),
authModules),
ec) {

// Uncomment the following lines if you want to prevent user with key to use password to authenticate
// override def authenticate(username: String, password: String)(implicit request: RequestHeader): Future[AuthContext] =
// userSrv.get(username)
// .transformWith {
// case Success(user) if user.key().isDefined ⇒ Future.failed(AuthenticationError("Authentication by password is not permitted for user with key"))
// case _: Success[_] ⇒ super.authenticate(username, password)
// case _: Failure[_] ⇒ Future.failed(AuthenticationError("Authentication failure"))
// }
}
1 change: 1 addition & 0 deletions thehive-backend/conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ PATCH /api/user/:userId controllers.UserCtrl.update(us
POST /api/user/:userId/password/set controllers.UserCtrl.setPassword(userId)
POST /api/user/:userId/password/change controllers.UserCtrl.changePassword(userId)
GET /api/user/:userId/key controllers.UserCtrl.getKey(userId)
DELETE /api/user/:userId/key controllers.UserCtrl.removeKey(userId)
POST /api/user/:userId/key/renew controllers.UserCtrl.renewKey(userId)


Expand Down

0 comments on commit deb0e2c

Please sign in to comment.