Skip to content

Commit

Permalink
#263 Add API key authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
To-om committed Sep 4, 2017
1 parent 213ea6c commit 5553200
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 80 deletions.
15 changes: 11 additions & 4 deletions thehive-backend/app/controllers/UserCtrl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,7 @@ class UserCtrl @Inject() (
@Timed
def get(id: String): Action[AnyContent] = authenticated(Roles.read).async { implicit request
userSrv.get(id)
.map { user
val json = if (request.roles.contains(Roles.admin)) user.toAdminJson else user.toJson
renderer.toOutput(OK, json)
}
.map { user renderer.toOutput(OK, user) }
}

@Timed
Expand Down Expand Up @@ -113,4 +110,14 @@ class UserCtrl @Inject() (
val (users, total) = userSrv.find(query, range, sort)
renderer.toOutput(OK, users, total)
}

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

@Timed
def renewKey(id: String): Action[AnyContent] = authenticated(Roles.admin).async { implicit request =>
authSrv.renewKey(id).map(Ok(_))
}
}
22 changes: 4 additions & 18 deletions thehive-backend/app/models/User.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package models

import java.util.UUID

import scala.concurrent.Future

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

import models.JsonFormat.userStatusFormat
import services.AuditedModel
Expand All @@ -21,7 +19,7 @@ trait UserAttributes { _: AttributeDef ⇒
val login = attribute("login", F.stringFmt, "Login of the user", O.form)
val userId = attribute("_id", F.stringFmt, "User id (login)", O.model)
val withKey = optionalAttribute("with-key", F.booleanFmt, "Generate an API key", O.form)
val key = optionalAttribute("key", F.uuidFmt, "API key", O.model, O.sensitive, O.unaudited)
val key = optionalAttribute("key", F.stringFmt, "API key", O.model, O.sensitive, O.unaudited)
val userName = attribute("name", F.stringFmt, "Full name (Firstname Lastname)")
val roles = multiAttribute("roles", RoleAttributeFormat, "Comma separated role list (READ, WRITE and ADMIN)")
val status = attribute("status", F.enumFmt(UserStatus), "Status of the user", UserStatus.Ok)
Expand All @@ -32,26 +30,14 @@ trait UserAttributes { _: AttributeDef ⇒

class UserModel extends ModelDef[UserModel, User]("user") with UserAttributes with AuditedModel {

private def addKey = (attrs: JsObject) attrs \ "with-key" match {
case _: JsUndefined attrs
case _ attrs + ("key" JsString(UUID.randomUUID.toString)) - "with-key"
}

private def setUserId = (attrs: JsObject) (attrs \ "login").asOpt[JsString].fold(attrs) { login
private def setUserId(attrs: JsObject) = (attrs \ "login").asOpt[JsString].fold(attrs) { login
attrs - "login" + ("_id" login)
}

override def creationHook(parent: Option[BaseEntity], attrs: JsObject) = Future.successful(addKey.andThen(setUserId)(attrs))

override def updateHook(user: BaseEntity, updateAttrs: JsObject): Future[JsObject] = Future.successful(addKey(updateAttrs))
override def creationHook(parent: Option[BaseEntity], attrs: JsObject): Future[JsObject] = Future.successful(setUserId(attrs))
}

class User(model: UserModel, attributes: JsObject) extends EntityDef[UserModel, User](model, attributes) with UserAttributes with org.elastic4play.services.User {
override def toJson = super.toJson +
("has-key" JsBoolean(key().isDefined))

def toAdminJson = key().fold(toJson) { k toJson + ("key" JsString(k.toString)) }

override def getUserName = userName()
override def getRoles = roles()
}
45 changes: 0 additions & 45 deletions thehive-backend/app/services/ApiKeyAuth.scala

This file was deleted.

46 changes: 33 additions & 13 deletions thehive-backend/app/services/LocalAuthSrv.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,25 @@ import javax.inject.{ Inject, Singleton }
import scala.concurrent.{ ExecutionContext, Future }
import scala.util.Random

import play.api.libs.json.{ JsObject, JsString }
import play.api.mvc.RequestHeader

import models.{ User, UserModel }
import akka.stream.Materializer
import akka.stream.scaladsl.Sink
import models.User

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

@Singleton
class LocalAuthSrv @Inject() (
userModel: UserModel,
userSrv: UserSrv,
updateSrv: UpdateSrv,
implicit val ec: ExecutionContext) extends AuthSrv {
implicit val ec: ExecutionContext,
implicit val mat: Materializer) extends AuthSrv {

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

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

def authenticate(username: String, password: String)(implicit request: RequestHeader): Future[AuthContext] = {
override def authenticate(username: String, password: String)(implicit request: RequestHeader): Future[AuthContext] = {
userSrv.get(username).flatMap { user
if (doAuthenticate(user, password)) userSrv.getFromUser(request, user)
else Future.failed(AuthenticationError("Authentication failure"))
}
}

def changePassword(username: String, oldPassword: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = {
override def authenticate(key: String)(implicit request: RequestHeader): Future[AuthContext] = {
import org.elastic4play.services.QueryDSL._
userSrv.find(and("status" ~= "Ok", "key" ~= key), Some("0-1"), Nil)
._1
.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)
else Future.failed(AuthorizationError("Authentication failure"))
}
}

def setPassword(username: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = {
override def setPassword(username: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = {
val seed = Random.nextString(10).replace(',', '!')
val newHash = seed + "," + Hasher("SHA-256").fromString(seed + newPassword).head.toString
userSrv.update(username, Fields(JsObject(Seq("password" JsString(newHash)))))
.map(_ ())
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")))
}

}
4 changes: 4 additions & 0 deletions thehive-backend/app/services/UserSrv.scala
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ class UserSrv @Inject() (
updateSrv[UserModel, User](userModel, id, fields)
}

def update(user: User, fields: Fields)(implicit Context: AuthContext): Future[User] = {
updateSrv(user, fields)
}

def delete(id: String)(implicit Context: AuthContext): Future[User] =
deleteSrv[UserModel, User](userModel, id)

Expand Down
3 changes: 3 additions & 0 deletions thehive-backend/conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ DELETE /api/user/:userId controllers.UserCtrl.delete(us
PATCH /api/user/:userId controllers.UserCtrl.update(userId)
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)
POST /api/user/:userId/key/renew controllers.UserCtrl.renewKey(userId)


POST /api/stream controllers.StreamCtrl.create()
GET /api/stream/status controllers.StreamCtrl.status
Expand Down

0 comments on commit 5553200

Please sign in to comment.