mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-12-27 02:39:53 +01:00
Merge pull request #1471 from gitbucket/feature/deploykeys
Deploy key support
This commit is contained in:
14
src/main/resources/update/gitbucket-core_4.11.xml
Normal file
14
src/main/resources/update/gitbucket-core_4.11.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<changeSet>
|
||||
<createTable tableName="DEPLOY_KEY">
|
||||
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||
<column name="DEPLOY_KEY_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
|
||||
<column name="TITLE" type="varchar(100)" nullable="false"/>
|
||||
<column name="PUBLIC_KEY" type="text" nullable="false"/>
|
||||
<column name="ALLOW_WRITE" type="boolean" nullable="false" defaultValueBoolean="false"/>
|
||||
</createTable>
|
||||
|
||||
<addPrimaryKey constraintName="IDX_DEPLOY_KEY_PK" tableName="DEPLOY_KEY" columnNames="USER_NAME, REPOSITORY_NAME, DEPLOY_KEY_ID"/>
|
||||
<addForeignKeyConstraint constraintName="IDX_DEPLOY_KEY_FK0" baseTableName="DEPLOY_KEY" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
|
||||
</changeSet>
|
||||
@@ -28,5 +28,8 @@ object GitBucketCoreModule extends Module("gitbucket-core",
|
||||
new Version("4.9.0",
|
||||
new LiquibaseMigration("update/gitbucket-core_4.9.xml")
|
||||
),
|
||||
new Version("4.10.0")
|
||||
new Version("4.10.0"),
|
||||
new Version("4.11.0",
|
||||
new LiquibaseMigration("update/gitbucket-core_4.11.xml")
|
||||
)
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.settings.html
|
||||
import gitbucket.core.model.WebHook
|
||||
import gitbucket.core.service.{RepositoryService, AccountService, WebHookService, ProtectedBranchService, CommitStatusService}
|
||||
import gitbucket.core.service._
|
||||
import gitbucket.core.service.WebHookService._
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.util.JGitUtil._
|
||||
@@ -19,11 +19,11 @@ import gitbucket.core.model.WebHookContentType
|
||||
|
||||
|
||||
class RepositorySettingsController extends RepositorySettingsControllerBase
|
||||
with RepositoryService with AccountService with WebHookService with ProtectedBranchService with CommitStatusService
|
||||
with RepositoryService with AccountService with WebHookService with ProtectedBranchService with CommitStatusService with DeployKeyService
|
||||
with OwnerAuthenticator with UsersAuthenticator
|
||||
|
||||
trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
self: RepositoryService with AccountService with WebHookService with ProtectedBranchService with CommitStatusService
|
||||
self: RepositoryService with AccountService with WebHookService with ProtectedBranchService with CommitStatusService with DeployKeyService
|
||||
with OwnerAuthenticator with UsersAuthenticator =>
|
||||
|
||||
// for repository options
|
||||
@@ -37,7 +37,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
externalWikiUrl: Option[String],
|
||||
allowFork: Boolean
|
||||
)
|
||||
|
||||
|
||||
val optionsForm = mapping(
|
||||
"repositoryName" -> trim(label("Repository Name" , text(required, maxlength(100), identifier, renameRepositoryName))),
|
||||
"description" -> trim(label("Description" , optional(text()))),
|
||||
@@ -56,12 +56,15 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100))))
|
||||
)(DefaultBranchForm.apply)
|
||||
|
||||
// // for collaborator addition
|
||||
// case class CollaboratorForm(userName: String)
|
||||
//
|
||||
// val collaboratorForm = mapping(
|
||||
// "userName" -> trim(label("Username", text(required, collaborator)))
|
||||
// )(CollaboratorForm.apply)
|
||||
|
||||
// for deploy key
|
||||
case class DeployKeyForm(title: String, publicKey: String, allowWrite: Boolean)
|
||||
|
||||
val deployKeyForm = mapping(
|
||||
"title" -> trim(label("Title", text(required, maxlength(100)))),
|
||||
"publicKey" -> trim(label("Key" , text(required))), // TODO duplication check in the repository?
|
||||
"allowWrite" -> trim(label("Key" , boolean()))
|
||||
)(DeployKeyForm.apply)
|
||||
|
||||
// for web hook url addition
|
||||
case class WebHookForm(url: String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])
|
||||
@@ -382,6 +385,24 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/danger")
|
||||
})
|
||||
|
||||
/** List deploy keys */
|
||||
get("/:owner/:repository/settings/deploykey")(ownerOnly { repository =>
|
||||
html.deploykey(repository, getDeployKeys(repository.owner, repository.name))
|
||||
})
|
||||
|
||||
/** Register a deploy key */
|
||||
post("/:owner/:repository/settings/deploykey", deployKeyForm)(ownerOnly { (form, repository) =>
|
||||
addDeployKey(repository.owner, repository.name, form.title, form.publicKey, form.allowWrite)
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/deploykey")
|
||||
})
|
||||
|
||||
/** Delete a deploy key */
|
||||
get("/:owner/:repository/settings/deploykey/delete/:id")(ownerOnly { repository =>
|
||||
val deployKeyId = params("id").toInt
|
||||
deleteDeployKey(repository.owner, repository.name, deployKeyId)
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/deploykey")
|
||||
})
|
||||
|
||||
/**
|
||||
* Provides duplication check for web hook url.
|
||||
*/
|
||||
|
||||
29
src/main/scala/gitbucket/core/model/DeployKey.scala
Normal file
29
src/main/scala/gitbucket/core/model/DeployKey.scala
Normal file
@@ -0,0 +1,29 @@
|
||||
package gitbucket.core.model
|
||||
|
||||
trait DeployKeyComponent { self: Profile =>
|
||||
import profile.api._
|
||||
|
||||
lazy val DeployKeys = TableQuery[DeployKeys]
|
||||
|
||||
class DeployKeys(tag: Tag) extends Table[DeployKey](tag, "DEPLOY_KEY") {
|
||||
val userName = column[String]("USER_NAME")
|
||||
val repositoryName = column[String]("REPOSITORY_NAME")
|
||||
val deployKeyId = column[Int]("DEPLOY_KEY_ID", O AutoInc)
|
||||
val title = column[String]("TITLE")
|
||||
val publicKey = column[String]("PUBLIC_KEY")
|
||||
val allowWrite = column[Boolean]("ALLOW_WRITE")
|
||||
def * = (userName, repositoryName, deployKeyId, title, publicKey, allowWrite) <> (DeployKey.tupled, DeployKey.unapply)
|
||||
|
||||
def byPrimaryKey(userName: String, repositoryName: String, deployKeyId: Int) =
|
||||
(this.userName === userName.bind) && (this.repositoryName === repositoryName.bind) && (this.deployKeyId === deployKeyId.bind)
|
||||
}
|
||||
}
|
||||
|
||||
case class DeployKey(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
deployKeyId: Int = 0,
|
||||
title: String,
|
||||
publicKey: String,
|
||||
allowWrite: Boolean
|
||||
)
|
||||
@@ -54,5 +54,6 @@ trait CoreProfile extends ProfileProvider with Profile
|
||||
with WebHookComponent
|
||||
with WebHookEventComponent
|
||||
with ProtectedBranchComponent
|
||||
with DeployKeyComponent
|
||||
|
||||
object Profile extends CoreProfile
|
||||
|
||||
31
src/main/scala/gitbucket/core/service/DeployKeyService.scala
Normal file
31
src/main/scala/gitbucket/core/service/DeployKeyService.scala
Normal file
@@ -0,0 +1,31 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import gitbucket.core.model.DeployKey
|
||||
import gitbucket.core.model.Profile._
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
|
||||
trait DeployKeyService {
|
||||
|
||||
def addDeployKey(userName: String, repositoryName: String, title: String, publicKey: String, allowWrite: Boolean)
|
||||
(implicit s: Session): Unit =
|
||||
DeployKeys.insert(DeployKey(
|
||||
userName = userName,
|
||||
repositoryName = repositoryName,
|
||||
title = title,
|
||||
publicKey = publicKey,
|
||||
allowWrite = allowWrite
|
||||
))
|
||||
|
||||
def getDeployKeys(userName: String, repositoryName: String)(implicit s: Session): List[DeployKey] =
|
||||
DeployKeys
|
||||
.filter(x => (x.userName === userName.bind) && (x.repositoryName === repositoryName.bind))
|
||||
.sortBy(_.deployKeyId).list
|
||||
|
||||
def getAllDeployKeys()(implicit s: Session): List[DeployKey] =
|
||||
DeployKeys.filter(_.publicKey.trim =!= "").list
|
||||
|
||||
def deleteDeployKey(userName: String, repositoryName: String, deployKeyId: Int)(implicit s: Session): Unit =
|
||||
DeployKeys.filter(_.byPrimaryKey(userName, repositoryName, deployKeyId)).delete
|
||||
|
||||
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
trait SshKeyService {
|
||||
|
||||
def addPublicKey(userName: String, title: String, publicKey: String)(implicit s: Session): Unit =
|
||||
SshKeys insert SshKey(userName = userName, title = title, publicKey = publicKey)
|
||||
SshKeys.insert(SshKey(userName = userName, title = title, publicKey = publicKey))
|
||||
|
||||
def getPublicKeys(userName: String)(implicit s: Session): List[SshKey] =
|
||||
SshKeys.filter(_.userName === userName.bind).sortBy(_.sshKeyId).list
|
||||
@@ -16,6 +16,6 @@ trait SshKeyService {
|
||||
SshKeys.filter(_.publicKey.trim =!= "").list
|
||||
|
||||
def deletePublicKey(userName: String, sshKeyId: Int)(implicit s: Session): Unit =
|
||||
SshKeys filter (_.byPrimaryKey(userName, sshKeyId)) delete
|
||||
SshKeys.filter(_.byPrimaryKey(userName, sshKeyId)).delete
|
||||
|
||||
}
|
||||
|
||||
@@ -175,9 +175,9 @@ object SystemSettingsService {
|
||||
fromName: Option[String])
|
||||
|
||||
case class SshAddress(
|
||||
host:String,
|
||||
port:Int,
|
||||
genericUser:String)
|
||||
host: String,
|
||||
port: Int,
|
||||
genericUser: String)
|
||||
|
||||
case class Lfs(
|
||||
serverUrl: Option[String])
|
||||
|
||||
@@ -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,68 @@ 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 isReadableUser(authType: AuthType, repositoryInfo: RepositoryService.RepositoryInfo)
|
||||
(implicit session: Session): Boolean = {
|
||||
authType match {
|
||||
case AuthType.UserAuthType(username) => {
|
||||
getAccountByUserName(username) match {
|
||||
case Some(account) => hasGuestRole(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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(x) if x.allowWrite => 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 || isReadableUser(authType, repositoryInfo)
|
||||
}.getOrElse(false)
|
||||
}
|
||||
|
||||
@@ -119,12 +155,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 +169,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 +179,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 +197,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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
<form method="POST" action="@context.path/@account.userName/_ssh" validate="true">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading strong">Add an SSH Key</div>
|
||||
<div class="panel-heading strong">Add a SSH Key</div>
|
||||
<div class="panel-body">
|
||||
<fieldset class="form-group">
|
||||
<label for="title" class="strong">Title</label>
|
||||
@@ -30,7 +30,7 @@
|
||||
<fieldset class="form-group">
|
||||
<label for="publicKey" class="strong">Key</label>
|
||||
<div><span id="error-publicKey" class="error"></span></div>
|
||||
<textarea name="publicKey" id="publicKey" class="form-control" style="height: 250px;"></textarea>
|
||||
<textarea name="publicKey" id="publicKey" class="form-control" style="height: 200px;"></textarea>
|
||||
</fieldset>
|
||||
<input type="submit" class="btn btn-success" value="Add"/>
|
||||
</div>
|
||||
|
||||
50
src/main/twirl/gitbucket/core/settings/deploykey.scala.html
Normal file
50
src/main/twirl/gitbucket/core/settings/deploykey.scala.html
Normal file
@@ -0,0 +1,50 @@
|
||||
@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo, deployKeys: List[gitbucket.core.model.DeployKey])(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.ssh.SshUtil
|
||||
@import gitbucket.core.view.helpers
|
||||
@gitbucket.core.html.main("Deploy keys", Some(repository)){
|
||||
@gitbucket.core.html.menu("settings", repository){
|
||||
@gitbucket.core.settings.html.menu("deploykeys", repository){
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading strong">Deploy keys</div>
|
||||
<div class="panel-body">
|
||||
@if(deployKeys.isEmpty){
|
||||
No keys
|
||||
}
|
||||
@deployKeys.zipWithIndex.map { case (key, i) =>
|
||||
@if(i != 0){
|
||||
<hr>
|
||||
}
|
||||
<strong style="line-height: 30px;">@key.title</strong> (@SshUtil.fingerPrint(key.publicKey).getOrElse("Key is invalid."))
|
||||
@if(key.allowWrite){
|
||||
<label class="label label-warning">Write access</label>
|
||||
}
|
||||
<a href="@helpers.url(repository)/settings/deploykey/delete/@key.deployKeyId" class="btn btn-sm btn-danger pull-right">Delete</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="@helpers.url(repository)/settings/deploykey" validate="true">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading strong">Add a deploy key</div>
|
||||
<div class="panel-body">
|
||||
<fieldset class="form-group">
|
||||
<label for="title" class="strong">Title</label>
|
||||
<div><span id="error-title" class="error"></span></div>
|
||||
<input type="text" name="title" id="title" class="form-control"/>
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<label for="publicKey" class="strong">Key</label>
|
||||
<div><span id="error-publicKey" class="error"></span></div>
|
||||
<textarea name="publicKey" id="publicKey" class="form-control" style="height: 200px;"></textarea>
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" name="allowWrite"> Allow write access
|
||||
</label>
|
||||
</fieldset>
|
||||
<input type="submit" class="btn btn-success" value="Add"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,9 @@
|
||||
<li@if(active=="hooks"){ class="active"}>
|
||||
<a href="@helpers.url(repository)/settings/hooks">Service Hooks</a>
|
||||
</li>
|
||||
<li@if(active=="deploykeys"){ class="active"}>
|
||||
<a href="@helpers.url(repository)/settings/deploykey">Deploy Keys</a>
|
||||
</li>
|
||||
<li@if(active=="danger"){ class="active"}>
|
||||
<a href="@helpers.url(repository)/settings/danger">Danger Zone</a>
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user