diff --git a/src/main/scala/gitbucket/core/ssh/GitCommand.scala b/src/main/scala/gitbucket/core/ssh/GitCommand.scala index 9620dbd..e3078e6 100644 --- a/src/main/scala/gitbucket/core/ssh/GitCommand.scala +++ b/src/main/scala/gitbucket/core/ssh/GitCommand.scala @@ -2,7 +2,7 @@ import gitbucket.core.model.Profile.profile.blockingApi._ import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry} -import gitbucket.core.service.{AccountService, RepositoryService, SystemSettingsService} +import gitbucket.core.service.{AccountService, DeployKeyService, RepositoryService, SystemSettingsService} import gitbucket.core.servlet.{CommitLogHook, Database} import gitbucket.core.util.{ControlUtil, Directory} import org.apache.sshd.server.{Command, CommandFactory, Environment, ExitCallback, SessionAware} @@ -13,6 +13,7 @@ import ControlUtil._ import org.eclipse.jgit.api.Git import Directory._ +import gitbucket.core.ssh.PublicKeyAuthenticator.AuthType import org.eclipse.jgit.transport.{ReceivePack, UploadPack} import org.apache.sshd.server.scp.UnknownCommand import org.eclipse.jgit.errors.RepositoryNotFoundException @@ -25,34 +26,33 @@ 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 + @volatile private var authType: Option[AuthType] = None - protected def runTask(authUser: String): Unit + protected def runTask(authType: AuthType): Unit - private def newTask(): Runnable = new Runnable { - override def run(): Unit = { - authUser match { - case Some(authUser) => - 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) - } + private def newTask(): Runnable = () => { + authType match { + case Some(authType) => + try { + runTask(authType) + 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) } } @@ -79,32 +79,50 @@ this.in = in } - override def setSession(serverSession:ServerSession) { - this.authUser = PublicKeyAuthenticator.getUserName(serverSession) + override def setSession(serverSession: ServerSession) { + this.authType = PublicKeyAuthenticator.getAuthType(serverSession) } } abstract class DefaultGitCommand(val owner: String, val repoName: String) extends GitCommand { - self: RepositoryService with AccountService => + self: RepositoryService with AccountService with DeployKeyService => - protected def isWritableUser(username: String, repositoryInfo: RepositoryService.RepositoryInfo) - (implicit session: Session): Boolean = - getAccountByUserName(username) match { - case Some(account) => hasDeveloperRole(repositoryInfo.owner, repositoryInfo.name, Some(account)) - case None => false + protected def userName(authType: AuthType): String = { + authType match { + case AuthType.UserAuthType(userName) => userName + case AuthType.DeployKeyType(_) => owner } + } + + protected def isWritableUser(authType: AuthType, repositoryInfo: RepositoryService.RepositoryInfo) + (implicit session: Session): Boolean = { + authType match { + case AuthType.UserAuthType(username) => { + getAccountByUserName(username) match { + case Some(account) => hasDeveloperRole(owner, repoName, Some(account)) + case None => false + } + } + case AuthType.DeployKeyType(key) => { + getDeployKeys(owner, repoName).filter(sshKey => SshUtil.str2PublicKey(sshKey.publicKey).exists(_ == key)) match { + case List(_) => true + case _ => false + } + } + } + } } class DefaultGitUploadPack(owner: String, repoName: String) extends DefaultGitCommand(owner, repoName) - with RepositoryService with AccountService { + with RepositoryService with AccountService with DeployKeyService { - override protected def runTask(user: String): Unit = { + override protected def runTask(authType: AuthType): Unit = { val execute = Database() withSession { implicit session => getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", "")).map { repositoryInfo => - !repositoryInfo.repository.isPrivate || isWritableUser(user, repositoryInfo) + !repositoryInfo.repository.isPrivate || isWritableUser(authType, repositoryInfo) }.getOrElse(false) } @@ -119,12 +137,12 @@ } class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) extends DefaultGitCommand(owner, repoName) - with RepositoryService with AccountService { + with RepositoryService with AccountService with DeployKeyService { - override protected def runTask(user: String): Unit = { + override protected def runTask(authType: AuthType): Unit = { val execute = Database() withSession { implicit session => getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", "")).map { repositoryInfo => - isWritableUser(user, repositoryInfo) + isWritableUser(authType, repositoryInfo) }.getOrElse(false) } @@ -133,7 +151,7 @@ val repository = git.getRepository val receive = new ReceivePack(repository) if (!repoName.endsWith(".wiki")) { - val hook = new CommitLogHook(owner, repoName, user, baseUrl) + val hook = new CommitLogHook(owner, repoName, userName(authType), baseUrl) receive.setPreReceiveHook(hook) receive.setPostReceiveHook(hook) } @@ -143,12 +161,11 @@ } } -class PluginGitUploadPack(repoName: String, routing: GitRepositoryRouting) extends GitCommand - with SystemSettingsService { +class PluginGitUploadPack(repoName: String, routing: GitRepositoryRouting) extends GitCommand with SystemSettingsService { - override protected def runTask(user: String): Unit = { + override protected def runTask(authType: AuthType): Unit = { val execute = Database() withSession { implicit session => - routing.filter.filter("/" + repoName, Some(user), loadSystemSettings(), false) + routing.filter.filter("/" + repoName, AuthType.userName(authType), loadSystemSettings(), false) } if(execute){ @@ -162,13 +179,13 @@ } } -class PluginGitReceivePack(repoName: String, routing: GitRepositoryRouting) extends GitCommand - with SystemSettingsService { +class PluginGitReceivePack(repoName: String, routing: GitRepositoryRouting) extends GitCommand with SystemSettingsService { - override protected def runTask(user: String): Unit = { + override protected def runTask(authType: AuthType): Unit = { val execute = Database() withSession { implicit session => - routing.filter.filter("/" + repoName, Some(user), loadSystemSettings(), true) + routing.filter.filter("/" + repoName, AuthType.userName(authType), loadSystemSettings(), true) } + if(execute){ val path = routing.urlPattern.r.replaceFirstIn(repoName, routing.localPath) using(Git.open(new File(Directory.GitBucketHome, path))){ git => diff --git a/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala b/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala index 0b249e1..814e3e9 100644 --- a/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala +++ b/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala @@ -2,73 +2,102 @@ import java.security.PublicKey -import gitbucket.core.service.SshKeyService +import gitbucket.core.service.{DeployKeyService, SshKeyService} import gitbucket.core.servlet.Database import gitbucket.core.model.Profile.profile.blockingApi._ +import gitbucket.core.ssh.PublicKeyAuthenticator.AuthType import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator import org.apache.sshd.server.session.ServerSession import org.apache.sshd.common.AttributeStore import org.slf4j.LoggerFactory object PublicKeyAuthenticator { + // put in the ServerSession here to be read by GitCommand later - private val userNameSessionKey = new AttributeStore.AttributeKey[String] + private val authTypeSessionKey = new AttributeStore.AttributeKey[AuthType] - def putUserName(serverSession: ServerSession, userName: String):Unit = - serverSession.setAttribute(userNameSessionKey, userName) + def putAuthType(serverSession: ServerSession, authType: AuthType):Unit = + serverSession.setAttribute(authTypeSessionKey, authType) - def getUserName(serverSession: ServerSession):Option[String] = - Option(serverSession.getAttribute(userNameSessionKey)) + def getAuthType(serverSession: ServerSession): Option[AuthType] = + Option(serverSession.getAttribute(authTypeSessionKey)) + + sealed trait AuthType + + object AuthType { + case class UserAuthType(userName: String) extends AuthType + case class DeployKeyType(publicKey: PublicKey) extends AuthType + + /** + * Retrieves username if authType is UserAuthType, otherwise None. + */ + def userName(authType: AuthType): Option[String] = { + authType match { + case UserAuthType(userName) => Some(userName) + case _ => None + } + } + } } -class PublicKeyAuthenticator(genericUser: String) extends PublickeyAuthenticator with SshKeyService { +class PublicKeyAuthenticator(genericUser: String) extends PublickeyAuthenticator with SshKeyService with DeployKeyService { 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) + override def authenticate(username: String, key: PublicKey, session: ServerSession): Boolean = { + Database().withSession { implicit s => + if (username == genericUser) { + authenticateGenericUser(username, key, session, genericUser) + } else { + authenticateLoginUser(username, key, session) + } } - else { - logger.info(s"authentication as ssh user ${username} failed") + } + + private def authenticateLoginUser(userName: String, key: PublicKey, session: ServerSession)(implicit s: Session): Boolean = { + val authenticated = getPublicKeys(userName).map(_.publicKey).flatMap(SshUtil.str2PublicKey).contains(key) + + if (authenticated) { + logger.info(s"authentication as ssh user ${userName} succeeded") + PublicKeyAuthenticator.putAuthType(session, AuthType.UserAuthType(userName)) + } else { + logger.info(s"authentication as ssh user ${userName} failed") } authenticated } - private def authenticateGenericUser(username: String, key: PublicKey, session: ServerSession, genericUser: String): Boolean = { + private def authenticateGenericUser(userName: String, key: PublicKey, session: ServerSession, genericUser: String)(implicit s: Session): Boolean = { // find all users having the key we got from ssh - val possibleUserNames = - Database() - .withSession { implicit dbSession => getAllKeys() } - .filter { sshKey => + val possibleUserNames = getAllKeys().filter { sshKey => + SshUtil.str2PublicKey(sshKey.publicKey).exists(_ == key) + }.map(_.userName).distinct + + // determine the user - if different accounts share the same key, tough luck + val uniqueUserName = possibleUserNames match { + case List(name) => Some(name) + case _ => None + } + + uniqueUserName.map { userName => + // found public key for user + logger.info(s"authentication as generic user ${genericUser} succeeded, identified ${userName}") + PublicKeyAuthenticator.putAuthType(session, AuthType.UserAuthType(userName)) + true + }.getOrElse { + // search deploy keys + val existsDeployKey = getAllDeployKeys().exists { sshKey => SshUtil.str2PublicKey(sshKey.publicKey).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 + if(existsDeployKey){ + // found deploy key for repository + PublicKeyAuthenticator.putAuthType(session, AuthType.DeployKeyType(key)) + logger.info(s"authentication as generic user ${genericUser} succeeded, deploy key was found") + true + } else { + // public key not found + logger.info(s"authentication by generic user ${genericUser} failed") + false } - uniqueUserName.foreach(PublicKeyAuthenticator.putUserName(session, _)) - uniqueUserName.isDefined + } } + }