gitbucket.core.util.LDAPUtil.scala Maven / Gradle / Ivy
The newest version!
package gitbucket.core.util
import gitbucket.core.model.Account
import gitbucket.core.service.SystemSettingsService
import gitbucket.core.service.SystemSettingsService.Ldap
import com.novell.ldap._
import java.security.{Provider, Security}
import java.util.concurrent.atomic.AtomicReference
import org.slf4j.LoggerFactory
import scala.annotation.tailrec
/**
* Utility for LDAP authentication.
*/
object LDAPUtil {
private val LDAP_VERSION: Int = LDAPConnection.LDAP_V3
private val LDAP_DUMMY_MAL = "@ldap-devnull"
private val logger = LoggerFactory.getLogger(getClass().getName())
private val provider = new AtomicReference[Provider](null)
/**
* Returns true if mail address ends with "@ldap-devnull"
*/
def isDummyMailAddress(account: Account): Boolean = {
account.mailAddress.endsWith(LDAP_DUMMY_MAL)
}
/**
* Creates dummy address (userName@ldap-devnull) for LDAP login.
*
* If mail address is not managed in LDAP server, GitBucket stores this dummy address in first LDAP login.
* GitBucket does not send any mails to this dummy address. And these users must input their mail address
* at the first step after LDAP authentication.
*/
def createDummyMailAddress(userName: String): String = {
userName + LDAP_DUMMY_MAL
}
/**
* Try authentication by LDAP using given configuration.
* Returns Right(LDAPUserInfo) if authentication is successful, otherwise Left(errorMessage).
*/
def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, LDAPUserInfo] = {
bind(
host = ldapSettings.host,
port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
dn = ldapSettings.bindDN.getOrElse(""),
password = ldapSettings.bindPassword.getOrElse(""),
tls = ldapSettings.tls.getOrElse(false),
ssl = ldapSettings.ssl.getOrElse(false),
keystore = ldapSettings.keystore.getOrElse(""),
error = "System LDAP authentication failed."
) { conn =>
findUser(
conn,
userName,
ldapSettings.baseDN,
ldapSettings.userNameAttribute,
ldapSettings.additionalFilterCondition
) match {
case Some(userDN) => userAuthentication(ldapSettings, userDN, userName, password)
case None => Left("User does not exist.")
}
}
}
private def userAuthentication(
ldapSettings: Ldap,
userDN: String,
userName: String,
password: String
): Either[String, LDAPUserInfo] = {
bind(
host = ldapSettings.host,
port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
dn = userDN,
password = password,
tls = ldapSettings.tls.getOrElse(false),
ssl = ldapSettings.ssl.getOrElse(false),
keystore = ldapSettings.keystore.getOrElse(""),
error = "User LDAP Authentication Failed."
) { conn =>
if (ldapSettings.mailAttribute.getOrElse("").isEmpty) {
Right(
LDAPUserInfo(
userName = userName,
fullName = ldapSettings.fullNameAttribute
.flatMap { fullNameAttribute =>
findFullName(conn, userDN, ldapSettings.userNameAttribute, userName, fullNameAttribute)
}
.getOrElse(userName),
mailAddress = createDummyMailAddress(userName)
)
)
} else {
findMailAddress(conn, userDN, ldapSettings.userNameAttribute, userName, ldapSettings.mailAttribute.get) match {
case Some(mailAddress) =>
Right(
LDAPUserInfo(
userName = getUserNameFromMailAddress(userName),
fullName = ldapSettings.fullNameAttribute
.flatMap { fullNameAttribute =>
findFullName(conn, userDN, ldapSettings.userNameAttribute, userName, fullNameAttribute)
}
.getOrElse(userName),
mailAddress = mailAddress
)
)
case None => Left("Can't find mail address.")
}
}
}
}
private def getUserNameFromMailAddress(userName: String): String = {
(userName.indexOf('@') match {
case i if i >= 0 => userName.substring(0, i)
case i => userName
}).replaceAll("[^a-zA-Z0-9\\-_.]", "").replaceAll("^[_\\-]", "")
}
private def getSslProvider(): Provider = {
import scala.language.existentials
val cachedInstance = provider.get()
if (cachedInstance == null) {
val cls =
try {
Class.forName("com.sun.net.ssl.internal.ssl.Provider")
} catch {
case e: ClassNotFoundException =>
Class.forName("com.ibm.jsse.IBMJSSEProvider")
case e: Throwable => throw e
}
val newInstance = cls
.getDeclaredConstructor()
.newInstance()
.asInstanceOf[Provider]
provider.compareAndSet(null, newInstance)
newInstance
} else {
cachedInstance
}
}
private def bind[A](
host: String,
port: Int,
dn: String,
password: String,
tls: Boolean,
ssl: Boolean,
keystore: String,
error: String
)(f: LDAPConnection => Either[String, A]): Either[String, A] = {
if (tls || ssl) {
// Dynamically set Sun as the security provider
Security.addProvider(getSslProvider())
if (keystore.compareTo("") != 0) {
// Dynamically set the property that JSSE uses to identify
// the keystore that holds trusted root certificates
System.setProperty("javax.net.ssl.trustStore", keystore)
}
}
val conn: LDAPConnection =
if (ssl) {
new LDAPConnection(new LDAPJSSESecureSocketFactory())
} else {
new LDAPConnection(new LDAPJSSEStartTLSFactory())
}
try {
// Connect to the server
conn.connect(host, port)
if (tls) {
// Secure the connection
conn.startTLS()
}
// Bind to the server
conn.bind(LDAP_VERSION, dn, password.getBytes)
// Execute a given function and returns a its result
f(conn)
} catch {
case e: Exception => {
// Provide more information if something goes wrong
logger.info("" + e)
if (conn.isConnected) {
conn.disconnect()
}
// Returns an error message
Left(error)
}
}
}
/**
* Search a specified user and returns userDN if exists.
*/
private def findUser(
conn: LDAPConnection,
userName: String,
baseDN: String,
userNameAttribute: String,
additionalFilterCondition: Option[String]
): Option[String] = {
@tailrec
def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = {
if (results.hasMore) {
getEntries(
results,
entries :+ (try {
Option(results.next)
} catch {
case ex: LDAPReferralException => None // NOTE(tanacasino): Referral follow is off. so ignores it.(for AD)
})
)
} else {
entries.flatten
}
}
val filterCond = additionalFilterCondition.getOrElse("") match {
case "" => userNameAttribute + "=" + userName
case x => "(&(" + x + ")(" + userNameAttribute + "=" + userName + "))"
}
getEntries(conn.search(baseDN, LDAPConnection.SCOPE_SUB, filterCond, null, false)).collectFirst { case x =>
x.getDN
}
}
private def findMailAddress(
conn: LDAPConnection,
userDN: String,
userNameAttribute: String,
userName: String,
mailAttribute: String
): Option[String] = {
val results = conn.search(
userDN,
LDAPConnection.SCOPE_BASE,
userNameAttribute + "=" + userName,
Array[String](mailAttribute),
false
)
if (results.hasMore) {
Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
} else None
}
private def findFullName(
conn: LDAPConnection,
userDN: String,
userNameAttribute: String,
userName: String,
nameAttribute: String
): Option[String] = {
val results = conn.search(
userDN,
LDAPConnection.SCOPE_BASE,
userNameAttribute + "=" + userName,
Array[String](nameAttribute),
false
)
if (results.hasMore) {
Option(results.next.getAttribute(nameAttribute)).map(_.getStringValue)
} else None
}
case class LDAPUserInfo(userName: String, fullName: String, mailAddress: String)
}