diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala index efc2e78..33bff37 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala @@ -417,9 +417,7 @@ def httpUrl(owner: String, name: String)(implicit context: Context): String = s"${context.baseUrl}/git/${owner}/${name}.git" def sshUrl(owner: String, name: String)(implicit context: Context): Option[String] = if(context.settings.ssh){ - context.loginAccount.flatMap { loginAccount => - context.settings.sshAddress.map { x => s"ssh://${loginAccount.userName}@${x.host}:${x.port}/${owner}/${name}.git" } - } + context.settings.sshAddress.map { x => s"ssh://${x.genericUser}@${x.host}:${x.port}/${owner}/${name}.git" } } else None def openRepoUrl(openUrl: String)(implicit context: Context): String = s"github-${context.platform}://openRepo/${openUrl}" diff --git a/src/main/scala/gitbucket/core/service/SshKeyService.scala b/src/main/scala/gitbucket/core/service/SshKeyService.scala index 4113d2c..fb317e9 100644 --- a/src/main/scala/gitbucket/core/service/SshKeyService.scala +++ b/src/main/scala/gitbucket/core/service/SshKeyService.scala @@ -12,6 +12,9 @@ def getPublicKeys(userName: String)(implicit s: Session): List[SshKey] = SshKeys.filter(_.userName === userName.bind).sortBy(_.sshKeyId).list + def getAllKeys()(implicit s: Session): List[SshKey] = + SshKeys.list + def deletePublicKey(userName: String, sshKeyId: Int)(implicit s: Session): Unit = SshKeys filter (_.byPrimaryKey(userName, sshKeyId)) delete diff --git a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala index b1f0f5c..668eb9a 100644 --- a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala +++ b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala @@ -141,7 +141,11 @@ for { host <- sshHost if ssh } - yield SshAddress(host, sshPort.getOrElse(DefaultSshPort)) + yield SshAddress( + host, + sshPort.getOrElse(DefaultSshPort), + "git" + ) } case class Ldap( @@ -169,7 +173,8 @@ case class SshAddress( host:String, - port:Int) + port:Int, + genericUser:String) val DefaultSshPort = 29418 val DefaultSmtpPort = 25 diff --git a/src/main/scala/gitbucket/core/ssh/GitCommand.scala b/src/main/scala/gitbucket/core/ssh/GitCommand.scala index eb12001..b4a76f7 100644 --- a/src/main/scala/gitbucket/core/ssh/GitCommand.scala +++ b/src/main/scala/gitbucket/core/ssh/GitCommand.scala @@ -5,7 +5,8 @@ import gitbucket.core.service.{RepositoryService, AccountService, SystemSettingsService} import gitbucket.core.servlet.{Database, CommitLogHook} import gitbucket.core.util.{Directory, ControlUtil} -import org.apache.sshd.server.{CommandFactory, Environment, ExitCallback, Command} +import org.apache.sshd.server.{CommandFactory, Environment, ExitCallback, Command, SessionAware} +import org.apache.sshd.server.session.ServerSession import org.slf4j.LoggerFactory import java.io.{File, InputStream, OutputStream} import ControlUtil._ @@ -20,37 +21,44 @@ val SimpleCommandRegex = """\Agit-(upload|receive)-pack '/(.+\.git)'\Z""".r } -abstract class GitCommand() extends Command { +abstract class GitCommand extends Command with SessionAware { private val logger = LoggerFactory.getLogger(classOf[GitCommand]) @volatile protected var err: OutputStream = null @volatile protected var in: InputStream = null @volatile protected var out: OutputStream = null @volatile protected var callback: ExitCallback = null + @volatile private var authUser:Option[String] = None - protected def runTask(user: String)(implicit session: Session): Unit + protected def runTask(authUser: String)(implicit session: Session): Unit - private def newTask(user: String): Runnable = new Runnable { + private def newTask(): Runnable = new Runnable { override def run(): Unit = { - Database() withSession { implicit session => - try { - runTask(user) - callback.onExit(0) - } catch { - case e: RepositoryNotFoundException => - logger.info(e.getMessage) - callback.onExit(1, "Repository Not Found") - case e: Throwable => - logger.error(e.getMessage, e) - callback.onExit(1) - } + authUser match { + case Some(authUser) => + Database() withSession { implicit session => + try { + runTask(authUser) + callback.onExit(0) + } catch { + case e: RepositoryNotFoundException => + logger.info(e.getMessage) + callback.onExit(1, "Repository Not Found") + case e: Throwable => + logger.error(e.getMessage, e) + callback.onExit(1) + } + } + case None => + val message = "User not authenticated" + logger.error(message) + callback.onExit(1, message) } } } - override def start(env: Environment): Unit = { - val user = env.getEnv.get("USER") - val thread = new Thread(newTask(user)) + final override def start(env: Environment): Unit = { + val thread = new Thread(newTask()) thread.start() } @@ -72,6 +80,10 @@ this.in = in } + override def setSession(serverSession:ServerSession) { + this.authUser = PublicKeyAuthenticator.getUserName(serverSession) + } + } abstract class DefaultGitCommand(val owner: String, val repoName: String) extends GitCommand { diff --git a/src/main/scala/gitbucket/core/ssh/NoShell.scala b/src/main/scala/gitbucket/core/ssh/NoShell.scala index 6f2f42d..47347fc 100644 --- a/src/main/scala/gitbucket/core/ssh/NoShell.scala +++ b/src/main/scala/gitbucket/core/ssh/NoShell.scala @@ -15,7 +15,6 @@ private var callback: ExitCallback = null override def start(env: Environment): Unit = { - val user = env.getEnv.get("USER") val message = """ | Welcome to @@ -32,7 +31,7 @@ | Please use: | | git clone ssh://%s@%s:%d/OWNER/REPOSITORY_NAME.git - """.stripMargin.format(user, sshAddress.host, sshAddress.port).replace("\n", "\r\n") + "\r\n" + """.stripMargin.format(sshAddress.genericUser, sshAddress.host, sshAddress.port).replace("\n", "\r\n") + "\r\n" err.write(Constants.encode(message)) err.flush() in.close() diff --git a/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala b/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala index 7d9eb23..507a8ee 100644 --- a/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala +++ b/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala @@ -2,22 +2,76 @@ import java.security.PublicKey +import gitbucket.core.model.SshKey import gitbucket.core.service.SshKeyService import gitbucket.core.servlet.Database import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator import org.apache.sshd.server.session.ServerSession +import org.apache.sshd.common.session.Session +import org.slf4j.LoggerFactory -class PublicKeyAuthenticator extends PublickeyAuthenticator with SshKeyService { +object PublicKeyAuthenticator { + // put in the ServerSession here to be read by GitCommand later + private val userNameSessionKey = new Session.AttributeKey[String] - override def authenticate(username: String, key: PublicKey, session: ServerSession): Boolean = { - Database() withSession { implicit session => - getPublicKeys(username).exists { sshKey => - SshUtil.str2PublicKey(sshKey.publicKey) match { - case Some(publicKey) => key.equals(publicKey) - case _ => false - } - } + def putUserName(serverSession:ServerSession, userName:String):Unit = + serverSession.setAttribute(userNameSessionKey, userName) + + def getUserName(serverSession:ServerSession):Option[String] = + Option(serverSession.getAttribute(userNameSessionKey)) +} + +class PublicKeyAuthenticator(genericUser:String) extends PublickeyAuthenticator with SshKeyService { + private val logger = LoggerFactory.getLogger(classOf[PublicKeyAuthenticator]) + + override def authenticate(username: String, key: PublicKey, session: ServerSession): Boolean = + if (username == genericUser) authenticateGenericUser(username, key, session, genericUser) + else authenticateLoginUser(username, key, session) + + private def authenticateLoginUser(username: String, key: PublicKey, session: ServerSession): Boolean = { + val authenticated = + Database() + .withSession { implicit dbSession => getPublicKeys(username) } + .map(_.publicKey) + .flatMap(SshUtil.str2PublicKey) + .contains(key) + if (authenticated) { + logger.info(s"authentication as ssh user ${username} succeeded") + PublicKeyAuthenticator.putUserName(session, username) } + else { + logger.info(s"authentication as ssh user ${username} failed") + } + authenticated } + private def authenticateGenericUser(username: String, key: PublicKey, session: ServerSession, genericUser:String): Boolean = { + // find all users having the key we got from ssh + val possibleUserNames = + Database() + .withSession { implicit dbSession => getAllKeys() } + .filter { sshKey => + Option(sshKey.publicKey) + .filter(_.trim.nonEmpty) + .flatMap(SshUtil.str2PublicKey) + .exists(_ == key) + } + .map(_.userName) + .distinct + // determine the user - if different accounts share the same key, tough luck + val uniqueUserName = + possibleUserNames match { + case List() => + logger.info(s"authentication as generic user ${genericUser} failed, public key not found") + None + case List(name) => + logger.info(s"authentication as generic user ${genericUser} succeeded, identified ${name}") + Some(name) + case _ => + logger.info(s"authentication as generic user ${genericUser} failed, public key is ambiguous") + None + } + uniqueUserName.foreach(PublicKeyAuthenticator.putUserName(session, _)) + uniqueUserName.isDefined + } } diff --git a/src/main/scala/gitbucket/core/ssh/SshServerListener.scala b/src/main/scala/gitbucket/core/ssh/SshServerListener.scala index 8795e40..6d3c123 100644 --- a/src/main/scala/gitbucket/core/ssh/SshServerListener.scala +++ b/src/main/scala/gitbucket/core/ssh/SshServerListener.scala @@ -21,7 +21,7 @@ provider.setAlgorithm("RSA") provider.setOverwriteAllowed(false) server.setKeyPairProvider(provider) - server.setPublickeyAuthenticator(new PublicKeyAuthenticator) + server.setPublickeyAuthenticator(new PublicKeyAuthenticator(sshAddress.genericUser)) server.setCommandFactory(new GitCommandFactory(baseUrl)) server.setShellFactory(new NoShell(sshAddress)) }