All Downloads are FREE. Search and download functionalities are using the official Maven repository.

no.kodeworks.kvarg.actor.AuthService.scala Maven / Gradle / Ivy

There is a newer version: 0.7
Show newest version
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