mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-12-24 09:20:04 +01:00
(refs #474)Add authentication and authronization by deploy key
This commit is contained in:
@@ -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 =>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user