no.kodeworks.kvarg.actor.AuthService.scala Maven / Gradle / Ivy
package no.kodeworks.kvarg.actor
import akka.actor.{Actor, ActorLogging, ActorRef}
import akka.event.LoggingAdapter
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.{AuthorizationFailedRejection, Directive1}
import akka.pattern.{ask, pipe}
import akka.util.Timeout
import no.kodeworks.kvarg.message.{InitFailure, InitSuccess}
import no.kodeworks.kvarg.actor.AuthService._
import com.tresata.akka.http.spnego.SpnegoDirectives._
import com.tresata.akka.http.spnego.Token
import com.typesafe.config.Config
import com.unboundid.ldap.sdk._
import scala.collection.JavaConverters._
import scala.util.{Success, Try}
class AuthService(
bootService: ActorRef,
timeout: Timeout,
hostname: String,
adminUser: String,
adminPassword: String,
baseDN: String,
// if None, upn is the same as hostname
userPrincipalName: Option[String] = None,
port0: Option[Int] = None,
active: Boolean = true
) extends Actor with ActorLogging {
implicit def to = timeout
val upn = userPrincipalName.getOrElse(hostname)
val port = port0.getOrElse(389)
val adminPrincipal = s"$adminUser@$upn"
override def preStart() {
log.info("Born")
super.preStart()
if (active) {
implicit def ec = context.dispatcher
(self ? BindAdmin).collect {
case BindAdminResponse(true) => InitSuccess
case _ => InitFailure
}.pipeTo(bootService)(self)
} else {
log.info("Inactive")
context.become(inactive)
bootService ! InitSuccess
}
}
override def postStop() {
log.info("Died")
}
val inactive: Receive = {
case BindAdmin =>
sender ! BindAdminResponse(false)
case AuthBySession(session) =>
sender ! AuthResponse(None)
case AuthByUser(user) =>
sender ! AuthResponse(None)
}
override def receive = {
case BindAdmin =>
sender ! BindAdminResponse(Try(createAdminBoundConnection().close).isSuccess)
case AuthBySession(session) =>
//TODO auth session cache
log.debug("Auth by session: " + session)
sender ! AuthResponse(None)
case AuthByUser(user) =>
log.debug("Auth by user: " + user)
val maybeAuth = doAuth(user)
log.debug("Auth response: " + maybeAuth)
sender ! AuthResponse(maybeAuth)
case x => log.info("Unhandled message: " + x)
}
def createAdminBoundConnection(): LDAPConnection = {
val connection = new LDAPConnection(hostname, port)
connection.bind(adminPrincipal, adminPassword)
connection
}
def doAuth(user0: String): Option[Auth] = Try {
val user = user0.split('@').head
val connection = createAdminBoundConnection()
val searchRes = connection.search(new SearchRequest(baseDN, SearchScope.SUB, s"(&(sAMAccountName=$user)(objectClass=user))"))
val auth = processSearch(user, searchRes)
connection.close
auth
}.toOption.flatten
def processSearch(username: String, res: SearchResult): Option[Auth] =
try {
if (ResultCode.SUCCESS != res.getResultCode) {
log.warning("Unsuccessfull auth for user {}", username)
None
} else if (1 != res.getEntryCount) {
log.warning("Wrong number of entry counts. Expected 1, got {}", res.getEntryCount)
None
} else {
res.getSearchEntries.asScala.headOption.map { entry =>
//TODO use all fields
val attrMap = List(
"givenName",
"sn",
"memberOf",
"mail",
"info",
"departmentNumber",
"extensionAttribute11",
"telephoneNumber",
"mobile",
"otherMobile",
"homePhone",
"ipPhone"
).map(key => key -> entry.getAttribute(key)).toMap
val firstname = attrMap("givenName").getValue
val lastname = attrMap("sn").getValue
val memberOfs = attrMap("memberOf").getValues.map(_.split(",").map(_.split("=")))
val roles = memberOfs.flatMap(
_.map(_.toList).collect {
case "CN" :: group :: _ => group.toUpperCase
}.filter(roleNames.contains)).map(Role).toList
val distrikt = Try(attrMap("departmentNumber").getValue.toInt).toOption.getOrElse(-1)
val mail = Try(attrMap("mail").getValue.toLowerCase).toOption.getOrElse("")
Auth(
username,
firstname,
lastname,
roles,
distrikt,
mail
)
}
}
}
catch {
case e: Exception =>
log.error(e, "Unknown error processing search results:")
None
}
}
object AuthService {
case class Props(auth: Auth)
case class Auth(
username: String,
firstname: String,
lastname: String,
roles: List[Role],
distrikt: Int,
email: String
)
object Auth {
val empty = Auth("dummyUser", "dummyFirstname", "dummyLastname", AuthService.roleNames.map(Role), 6, "[email protected]")
}
case class Role(name: String)
sealed trait Message
case object BindAdmin extends Message
case class BindAdminResponse(ok: Boolean = true)
case class AuthByUser(user: String) extends Message
case class AuthBySession(session: String) extends Message
case class AuthResponse(auth: Option[Auth])
val roleNames = List.empty[String]
def processAuthResponse(
authMessage: Message,
authService: ActorRef,
timeout: Timeout
): Directive1[Auth] = {
implicit def to = timeout
onComplete((authService ? authMessage).mapTo[AuthResponse]).flatMap {
case Success(AuthResponse(Some(auth))) => provide(auth)
case _ => reject(AuthorizationFailedRejection)
}
}
def auth(
session: String,
authService: ActorRef,
spnegoConfig: Option[Config],
timeout: Timeout,
log: LoggingAdapter
): Directive1[Auth] = {
if (spnegoConfig.isEmpty) {
log.debug(s"Spnego config empty, providing empty auth: ${Auth.empty}")
provide(Auth.empty)
}
else
authBySession(session, authService, timeout) |
spnegoAuthenticate(spnegoConfig.get).flatMap(
authByToken(_, authService, timeout))
}
def authByToken(
token: Token,
authService: ActorRef,
timeout: Timeout
): Directive1[Auth] = {
if (token.expired) {
println(s"token expired TODO correct rejection: $token")
reject(AuthorizationFailedRejection)
}
else authByUser(token.principal, authService, timeout)
}
def authByUser(user: String, authService: ActorRef, timeout: Timeout): Directive1[Auth] =
processAuthResponse(AuthByUser(user), authService, timeout)
def authBySession(session: String, authService: ActorRef, timeout: Timeout): Directive1[Auth] =
processAuthResponse(AuthBySession(session), authService, timeout)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy