diff --git a/project/build.scala b/project/build.scala index 3b0cefb..c5cdd89 100644 --- a/project/build.scala +++ b/project/build.scala @@ -36,6 +36,7 @@ "org.pegdown" % "pegdown" % "1.3.0", "org.apache.commons" % "commons-compress" % "1.5", "com.typesafe.slick" %% "slick" % "1.0.1", + "com.novell.ldap" % "jldap" % "2009-10-07", "com.h2database" % "h2" % "1.3.171", "ch.qos.logback" % "logback-classic" % "1.0.6" % "runtime", "org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container", diff --git a/src/main/scala/app/SignInController.scala b/src/main/scala/app/SignInController.scala index d7e8248..dea995a 100644 --- a/src/main/scala/app/SignInController.scala +++ b/src/main/scala/app/SignInController.scala @@ -1,7 +1,6 @@ package app import service._ -import util.StringUtil._ import jp.sf.amateras.scalatra.forms._ class SignInController extends SignInControllerBase with SystemSettingsService with AccountService @@ -24,19 +23,11 @@ } post("/signin", form){ form => - getAccountByUserName(form.userName).collect { - case account if(!account.isGroupAccount && account.password == sha1(form.password)) => { - session.setAttribute("LOGIN_ACCOUNT", account) - updateLastLoginDate(account.userName) - - session.get("REDIRECT").map { redirectUrl => - session.removeAttribute("REDIRECT") - redirect(redirectUrl.asInstanceOf[String]) - }.getOrElse { - redirect("/") - } - } - } getOrElse redirect("/signin") + val settings = loadSystemSettings() + authenticate(loadSystemSettings(), form.userName, form.password) match { + case Some(account) => signin(account) + case None => redirect("/signin") + } } get("/signout"){ @@ -44,4 +35,19 @@ redirect("/") } + /** + * Set account information into HttpSession and redirect. + */ + private def signin(account: model.Account) = { + session.setAttribute("LOGIN_ACCOUNT", account) + updateLastLoginDate(account.userName) + + session.get("REDIRECT").map { redirectUrl => + session.removeAttribute("REDIRECT") + redirect(redirectUrl.asInstanceOf[String]) + }.getOrElse { + redirect("/") + } + } + } \ No newline at end of file diff --git a/src/main/scala/app/SystemSettingsController.scala b/src/main/scala/app/SystemSettingsController.scala index 0e824c4..d1a813e 100644 --- a/src/main/scala/app/SystemSettingsController.scala +++ b/src/main/scala/app/SystemSettingsController.scala @@ -17,12 +17,22 @@ "gravatar" -> trim(label("Gravatar", boolean())), "notification" -> trim(label("Notification", boolean())), "smtp" -> optionalIfNotChecked("notification", mapping( - "host" -> trim(label("SMTP Host", text(required))), - "port" -> trim(label("SMTP Port", optional(number()))), - "user" -> trim(label("SMTP User", optional(text()))), - "password" -> trim(label("SMTP Password", optional(text()))), - "ssl" -> trim(label("Enable SSL", optional(boolean()))) - )(Smtp.apply)) + "host" -> trim(label("SMTP Host", text(required))), + "port" -> trim(label("SMTP Port", optional(number()))), + "user" -> trim(label("SMTP User", optional(text()))), + "password" -> trim(label("SMTP Password", optional(text()))), + "ssl" -> trim(label("Enable SSL", optional(boolean()))) + )(Smtp.apply)), + "ldapAuthentication" -> trim(label("LDAP", boolean())), + "ldap" -> optionalIfNotChecked("ldapAuthentication", mapping( + "host" -> trim(label("LDAP host", text(required))), + "port" -> trim(label("LDAP port", optional(number()))), + "bindDN" -> trim(label("Bind DN", text(required))), + "bindPassword" -> trim(label("Bind Password", text(required))), + "baseDN" -> trim(label("Base DN", text(required))), + "userNameAttribute" -> trim(label("User name attribute", text(required))), + "mailAttribute" -> trim(label("Mail address attribute", text(required))) + )(Ldap.apply)) )(SystemSettings.apply) diff --git a/src/main/scala/service/AccountService.scala b/src/main/scala/service/AccountService.scala index 3f828e3..39ddf6b 100644 --- a/src/main/scala/service/AccountService.scala +++ b/src/main/scala/service/AccountService.scala @@ -3,9 +3,48 @@ import model._ import scala.slick.driver.H2Driver.simple._ import Database.threadLocalSession +import service.SystemSettingsService.SystemSettings +import util.StringUtil._ +import model.GroupMember +import scala.Some +import model.Account +import util.LDAPUtil trait AccountService { + def authenticate(settings: SystemSettings, userName: String, password: String): Option[Account] = + if(settings.ldapAuthentication){ + ldapAuthentication(settings, userName, password) + } else { + defaultAuthentication(userName, password) + } + + /** + * Authenticate by internal database. + */ + private def defaultAuthentication(userName: String, password: String) = { + getAccountByUserName(userName).collect { + case account if(!account.isGroupAccount && account.password == sha1(password)) => Some(account) + } getOrElse None + } + + /** + * Authenticate by LDAP. + */ + private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) = { + LDAPUtil.authenticate(settings.ldap.get, userName, password) match { + case Right(mailAddress) => { + // Create or update account by LDAP information + getAccountByUserName(userName) match { + case Some(x) => updateAccount(x.copy(mailAddress = mailAddress)) + case None => createAccount(userName, "", mailAddress, false, None) + } + getAccountByUserName(userName) + } + case Left(errorMessage) => defaultAuthentication(userName, password) + } + } + def getAccountByUserName(userName: String): Option[Account] = Query(Accounts) filter(_.userName is userName.bind) firstOption diff --git a/src/main/scala/service/SystemSettingsService.scala b/src/main/scala/service/SystemSettingsService.scala index 6837f70..4c01135 100644 --- a/src/main/scala/service/SystemSettingsService.scala +++ b/src/main/scala/service/SystemSettingsService.scala @@ -19,6 +19,18 @@ smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString)) } } + props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString) + if(settings.ldapAuthentication){ + settings.ldap.map { ldap => + props.setProperty(LdapHost, ldap.host) + ldap.port.foreach(x => props.setProperty(LdapPort, x.toString)) + props.setProperty(LdapBindDN, ldap.bindDN) + props.setProperty(LdapBindPassword, ldap.bindPassword) + props.setProperty(LdapBaseDN, ldap.baseDN) + props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute) + props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute) + } + } props.store(new java.io.FileOutputStream(GitBucketConf), null) } @@ -41,6 +53,19 @@ getOptionValue[Boolean](props, SmtpSsl, None))) } else { None + }, + getValue(props, LdapAuthentication, false), + if(getValue(props, LdapAuthentication, false)){ + Some(Ldap( + getValue(props, LdapHost, ""), + getOptionValue(props, LdapPort, Some(DefaultLdapPort)), + getValue(props, LdapBindDN, ""), + getValue(props, LdapBindPassword, ""), + getValue(props, LdapBaseDN, ""), + getValue(props, LdapUserNameAttribute, ""), + getValue(props, LdapMailAddressAttribute, ""))) + } else { + None } ) } @@ -54,8 +79,19 @@ allowAccountRegistration: Boolean, gravatar: Boolean, notification: Boolean, - smtp: Option[Smtp] - ) + smtp: Option[Smtp], + ldapAuthentication: Boolean, + ldap: Option[Ldap]) + + case class Ldap( + host: String, + port: Option[Int], + bindDN: String, + bindPassword: String, + baseDN: String, + userNameAttribute: String, + mailAttribute: String) + case class Smtp( host: String, port: Option[Int], @@ -63,6 +99,8 @@ password: Option[String], ssl: Option[Boolean]) + val DefaultLdapPort = 389 + private val AllowAccountRegistration = "allow_account_registration" private val Gravatar = "gravatar" private val Notification = "notification" @@ -71,6 +109,14 @@ private val SmtpUser = "smtp.user" private val SmtpPassword = "smtp.password" private val SmtpSsl = "smtp.ssl" + private val LdapAuthentication = "ldap_authentication" + private val LdapHost = "ldap.host" + private val LdapPort = "ldap.port" + private val LdapBindDN = "ldap.bindDN" + private val LdapBindPassword = "ldap.bind_password" + private val LdapBaseDN = "ldap.baseDN" + private val LdapUserNameAttribute = "ldap.username_attribute" + private val LdapMailAddressAttribute = "ldap.mail_attribute" private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = { val value = props.getProperty(key) diff --git a/src/main/scala/servlet/BasicAuthenticationFilter.scala b/src/main/scala/servlet/BasicAuthenticationFilter.scala index 51d6618..6dda923 100644 --- a/src/main/scala/servlet/BasicAuthenticationFilter.scala +++ b/src/main/scala/servlet/BasicAuthenticationFilter.scala @@ -2,14 +2,13 @@ import javax.servlet._ import javax.servlet.http._ -import util.StringUtil._ -import service.{AccountService, RepositoryService} +import service.{SystemSettingsService, AccountService, RepositoryService} import org.slf4j.LoggerFactory /** * Provides BASIC Authentication for [[servlet.GitRepositoryServlet]]. */ -class BasicAuthenticationFilter extends Filter with RepositoryService with AccountService { +class BasicAuthenticationFilter extends Filter with RepositoryService with AccountService with SystemSettingsService { private val logger = LoggerFactory.getLogger(classOf[BasicAuthenticationFilter]) @@ -58,12 +57,12 @@ } } - private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean = { - getAccountByUserName(username).map { account => - account.password == sha1(password) && hasWritePermission(repository.owner, repository.name, Some(account)) - } getOrElse false - } - + private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean = + authenticate(loadSystemSettings(), username, password) match { + case Some(account) => hasWritePermission(repository.owner, repository.name, Some(account)) + case None => false + } + private def requireAuth(response: HttpServletResponse): Unit = { response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"") response.sendError(HttpServletResponse.SC_UNAUTHORIZED) diff --git a/src/main/scala/util/LDAPUtil.scala b/src/main/scala/util/LDAPUtil.scala new file mode 100644 index 0000000..bb31f32 --- /dev/null +++ b/src/main/scala/util/LDAPUtil.scala @@ -0,0 +1,97 @@ +package util + +import service.SystemSettingsService.Ldap +import service.SystemSettingsService +import com.novell.ldap.{LDAPReferralException, LDAPEntry, LDAPConnection} + +/** + * Utility for LDAP authentication. + */ +object LDAPUtil { + + private val LDAP_VERSION: Int = 3 + + /** + * Try authentication by LDAP using given configuration. + * Returns Right(mailAddress) if authentication is successful, otherwise Left(errorMessage). + */ + def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, String] = { + bind( + ldapSettings.host, + ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort), + ldapSettings.bindDN, + ldapSettings.bindPassword + ) match { + case Some(conn) => { + withConnection(conn) { conn => + findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match { + case Some(userDN) => userAuthentication(ldapSettings, userDN, password) + case None => Left("User does not exist") + } + } + } + case None => Left("System LDAP authentication failed.") + } + } + + private def userAuthentication(ldapSettings: Ldap, userDN: String, password: String): Either[String, String] = { + bind( + ldapSettings.host, + ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort), + userDN, + password + ) match { + case Some(conn) => { + withConnection(conn) { conn => + findMailAddress(conn, userDN, ldapSettings.mailAttribute) match { + case Some(mailAddress) => Right(mailAddress) + case None => Left("Can't find mail address.") + } + } + } + case None => Left("User LDAP Authentication Failed.") + } + } + + private def bind(host: String, port: Int, dn: String, password: String): Option[LDAPConnection] = { + val conn: LDAPConnection = new LDAPConnection + try { + conn.connect(host, port) + conn.bind(LDAP_VERSION, dn, password.getBytes) + Some(conn) + } catch { + case e: Exception => { + if (conn.isConnected) conn.disconnect() + None + } + } + } + + private def withConnection[T](conn: LDAPConnection)(f: LDAPConnection => T): T = { + try { + f(conn) + } finally { + conn.disconnect() + } + } + + private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String): Option[String] = { + val results = conn.search(baseDN, LDAPConnection.SCOPE_SUB, userNameAttribute + "=" + userName, null, false) + (for(i <- 0 to results.getCount) yield try { + Some(results.next) + } catch { + case ex: LDAPReferralException => None // NOTE(tanacasino): Referral follow is off. so ignores it.(for AD) + }).flatten.collectFirst { + case x if(x != null) => x.getDN + } + } + + private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] = { + val results = conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false) + if (results.hasMore) { + Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue) + } else { + None + } + } +} diff --git a/src/main/twirl/account/edit.scala.html b/src/main/twirl/account/edit.scala.html index 2eeb22a..24e0eaf 100644 --- a/src/main/twirl/account/edit.scala.html +++ b/src/main/twirl/account/edit.scala.html @@ -18,15 +18,17 @@ } -
+ @if(account.map(_.password.nonEmpty).getOrElse(true)){ + + }