app/auth/LDAP.scala (176 lines of code) (raw):
/*
* Copyright (C) 2015 Jason Mar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Modified by Andy Gallagher to improve error reporting when configuration is invalid
*/
package auth
import com.unboundid.ldap.sdk._
import com.unboundid.util.ssl.{SSLUtil, TrustAllTrustManager, TrustStoreTrustManager}
import java.security.MessageDigest
import Conf._
import play.api.Logger
import play.api.cache.SyncCacheApi
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.util.{Failure, Success, Try}
import scala.concurrent.ExecutionContext.Implicits.global
object LDAP {
private val logger = Logger(getClass)
protected val trustManager = {
(ldapProtocol,ldapUseKeystore) match {
case ("ldaps",true) =>
Try { new TrustStoreTrustManager(trustStore,trustStorePass,trustStoreType,true) }
case ("ldaps",false) =>
Try { new TrustAllTrustManager() }
case _ =>
Success(null)// don't need a trust store
}
}
// Initialize Multi-Server LDAP Connection Pool
val connectionPool:Try[LDAPConnectionPool] = ldapProtocol match {
case "ldaps" =>
Try { new LDAPConnectionPool(new FailoverServerSet(serverAddresses, serverPorts,new SSLUtil(trustManager.get).createSSLSocketFactory()),new SimpleBindRequest(bindDN, bindPass), poolSize) }
case "ldap" =>
Try { new LDAPConnectionPool(new FailoverServerSet(serverAddresses, serverPorts),new SimpleBindRequest(bindDN, bindPass), poolSize) }
case _ =>
Failure(new RuntimeException(s"Invalid ldap protocol in settings: $ldapProtocol"))
}
def getDN (searchEntries: java.util.List[com.unboundid.ldap.sdk.SearchResultEntry]) : Option[String] = {
searchEntries.size match {
case 0 => None
case _ => Some(searchEntries.get(0).getDN)
}
}
def getUserDN (uid:String)(implicit cache:SyncCacheApi) : Option[String] = {
if(connectionPool.isFailure) return None
val cacheKey = "userDN." + uid
val userDN: Option[String] = cache.getOrElseUpdate[Option[String]](cacheKey) {
logger.debug("LDAP: get DN for " + uid)
// Get DN for a given uid
val searchEntries : java.util.List[com.unboundid.ldap.sdk.SearchResultEntry] = connectionPool.get
.search(new SearchRequest(
userBaseDN,
SearchScope.SUB,
Filter.createEqualityFilter(uidAttribute,uid))
)
.getSearchEntries
getDN(searchEntries)
}
userDN
}
/**
* Returns false if the connection pool has either failed or there are no available connections
* @return
*/
def hasConnectionPool:Future[Try[Unit]] = Future {
connectionPool
.map(_.getCurrentAvailableConnections>0)
.getOrElse(false) match {
case true=>Success(())
case false=>Failure(new RuntimeException("No LDAP pool connections available"))
}
}
def getUserRoles (uid: String)(implicit cache:SyncCacheApi) : Option[List[String]] = {
if(connectionPool.isFailure) return None
val cacheKey = "userRoles." + uid
logger.debug(s"cacheKey: $cacheKey")
logger.debug(s"cache: $cache")
val userRoles : Option[List[String]] = cache.getOrElseUpdate[Option[List[String]]](cacheKey,Duration.create(ldapCacheDuration,"seconds")) {
logger.debug("LDAP: get roles for " + uid)
try {
val searchEntries : java.util.List[com.unboundid.ldap.sdk.SearchResultEntry] = connectionPool.get
.search(new SearchRequest(
userBaseDN,
SearchScope.SUB,
Filter.createEqualityFilter(uidAttribute,uid),roleMemberAttribute)
)
.getSearchEntries
val groups : List[String] = searchEntries.get(0)
.getAttributeValues("memberOf")
.toList
.map { _.split(",")(0).split("=")(1) }
logger.debug(s"Got roles $groups")
Some(groups)
} catch {
case ex:java.lang.IndexOutOfBoundsException=>
logger.error(s"User $uid has no roles attached?", ex)
None
case lde: LDAPException =>
logger.error("Could not look up ldap groups", lde)
None
}
}
logger.debug(s"Got user roles $userRoles")
userRoles
}
def getRoleDN (role:String)(implicit cache:SyncCacheApi) : Option[String] = {
if(connectionPool.isFailure) return None
val cacheKey = "roleDN." + role
val roleDN : Option[String] = cache.getOrElseUpdate[Option[String]](cacheKey) {
logger.debug("LDAP: get DN for " + role)
// Get DN for a given role
val searchEntries : java.util.List[com.unboundid.ldap.sdk.SearchResultEntry] = connectionPool.get
.search(new SearchRequest(
roleBaseDN,
SearchScope.SUB,
Filter.createEqualityFilter(roleAttribute,role))
)
.getSearchEntries
getDN(searchEntries)
}
roleDN
}
def compareMember (roleDN: String, userDN: String) : Int = {
if(connectionPool.isFailure) return -1
logger.debug("LDAP: compare " + roleDN + " " + userDN)
connectionPool.get
.compare(new CompareRequest(roleDN,memberAttribute,userDN))
.getResultCode
.intValue
}
def isMember (role:String, uid:String)(implicit cache:SyncCacheApi, connectionPool:LDAPConnectionPool) : Int = {
// Check if a given uid is a member of specified role
val roleDN : Option[String] = getRoleDN(role)
val userDN : Option[String] = getUserDN(uid)
(roleDN,userDN) match {
case (Some(r),Some(u)) =>
compareMember(r,u)
case (None,Some(u)) =>
201 // role not found
case (Some(r),None) =>
202 // uid not found
case (None,None) =>
203 // role and uid not found
}
}
def bind (uid:String,pass:String)(implicit cache:SyncCacheApi) : Try[Int] = Try {
if(connectionPool.isFailure) return Failure(connectionPool.failed.get)
val msg : String = uid + pass + "ela_salt_201406"
val hash : String = MessageDigest.getInstance("SHA-256")
.digest(msg.getBytes)
.foldLeft("")(
(s: String, b: Byte) =>
s + Character.forDigit((b & 0xf0) >> 4, 16) + Character.forDigit(b & 0x0f, 16)
)
val cacheKey = "bindResult." + hash
val bindResult : Int = cache.getOrElseUpdate[Int](cacheKey,Duration(ldapCacheDuration,"seconds")) {
getUserDN(uid) match {
case Some(dn) =>
logger.debug("LDAP: binding " + uid + " hash=" + hash)
connectionPool.get
.bindAndRevertAuthentication(new SimpleBindRequest(dn,pass))
.getResultCode
.intValue()
case _ => 1
}
}
bindResult
}
def getFullName (uid:String)(implicit cache:SyncCacheApi) : String = {
if(connectionPool.isFailure) return ""
val cacheKey = "userFullName." + uid
val userFullName : String = cache.getOrElseUpdate[String](cacheKey,Duration(ldapCacheDuration,"seconds")) {
logger.debug("LDAP: search " + uid + " Full Name")
connectionPool.get
.search(
new SearchRequest(
userBaseDN,
SearchScope.SUB,
Filter.createEqualityFilter(uidAttribute,uid),
"name"
)
)
.getSearchEntries
.get(0)
.getAttributeValue("name")
}
userFullName
}
}