diff --git a/thehive-backend/app/controllers/UserCtrl.scala b/thehive-backend/app/controllers/UserCtrl.scala index a8a7395be6..d7922402c9 100644 --- a/thehive-backend/app/controllers/UserCtrl.scala +++ b/thehive-backend/app/controllers/UserCtrl.scala @@ -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(_)) diff --git a/thehive-backend/app/global/TheHive.scala b/thehive-backend/app/global/TheHive.scala index c6a2400d33..a7d4b0c67a 100644 --- a/thehive-backend/app/global/TheHive.scala +++ b/thehive-backend/app/global/TheHive.scala @@ -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 @@ -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, @@ -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") @@ -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) @@ -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") diff --git a/thehive-backend/app/models/User.scala b/thehive-backend/app/models/User.scala index f36bfd88ba..f092b0801e 100644 --- a/thehive-backend/app/models/User.scala +++ b/thehive-backend/app/models/User.scala @@ -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 @@ -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)) } \ No newline at end of file diff --git a/thehive-backend/app/services/KeyAuthSrv.scala b/thehive-backend/app/services/KeyAuthSrv.scala new file mode 100644 index 0000000000..2bcc301aba --- /dev/null +++ b/thehive-backend/app/services/KeyAuthSrv.scala @@ -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(_ ⇒ ()) + } +} diff --git a/thehive-backend/app/services/LocalAuthSrv.scala b/thehive-backend/app/services/LocalAuthSrv.scala index 334ff1d737..fe96e383e5 100644 --- a/thehive-backend/app/services/LocalAuthSrv.scala +++ b/thehive-backend/app/services/LocalAuthSrv.scala @@ -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() ( @@ -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) { @@ -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) @@ -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"))) - } - } \ No newline at end of file diff --git a/thehive-backend/app/services/TheHiveAuthSrv.scala b/thehive-backend/app/services/TheHiveAuthSrv.scala new file mode 100644 index 0000000000..dfc27cadc7 --- /dev/null +++ b/thehive-backend/app/services/TheHiveAuthSrv.scala @@ -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")) + // } +} \ No newline at end of file diff --git a/thehive-backend/conf/routes b/thehive-backend/conf/routes index fcaba89198..7ce66025ca 100644 --- a/thehive-backend/conf/routes +++ b/thehive-backend/conf/routes @@ -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)