(refs #474)Add authentication and authronization by deploy key

This commit is contained in:
Naoki Takezoe
2017-03-03 10:47:21 +09:00
parent 629aaa78d6
commit b5f287d75e
2 changed files with 136 additions and 90 deletions

View File

@@ -2,7 +2,7 @@ package gitbucket.core.ssh
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 java.io.{File, InputStream, OutputStream}
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 @@ object GitCommand {
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 @@ abstract class GitCommand extends Command with SessionAware {
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 DefaultGitUploadPack(owner: String, repoName: String) extends DefaultGitCo
}
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 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) ex
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 DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) ex
}
}
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 PluginGitUploadPack(repoName: String, routing: GitRepositoryRouting) exten
}
}
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 =>

View File

@@ -2,73 +2,102 @@ package gitbucket.core.ssh
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
}
}
}