From 42d3585df567b93b76bf24ece71a8efad1aeeb97 Mon Sep 17 00:00:00 2001 From: Naoki Takezoe Date: Thu, 2 Mar 2017 16:48:54 +0900 Subject: [PATCH 1/6] (refs #474)Define DB and models for deploy key support --- .../resources/update/gitbucket-core_4.11.xml | 13 +++++++++ .../gitbucket/core/GitBucketCoreModule.scala | 5 +++- .../gitbucket/core/model/DeployKey.scala | 27 +++++++++++++++++++ .../scala/gitbucket/core/model/Profile.scala | 1 + 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/update/gitbucket-core_4.11.xml create mode 100644 src/main/scala/gitbucket/core/model/DeployKey.scala diff --git a/src/main/resources/update/gitbucket-core_4.11.xml b/src/main/resources/update/gitbucket-core_4.11.xml new file mode 100644 index 000000000..24f1f430b --- /dev/null +++ b/src/main/resources/update/gitbucket-core_4.11.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala index 4b9c486c3..dd8665d55 100644 --- a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala +++ b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala @@ -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") + ) ) diff --git a/src/main/scala/gitbucket/core/model/DeployKey.scala b/src/main/scala/gitbucket/core/model/DeployKey.scala new file mode 100644 index 000000000..829f25624 --- /dev/null +++ b/src/main/scala/gitbucket/core/model/DeployKey.scala @@ -0,0 +1,27 @@ +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") + def * = (userName, repositoryName, deployKeyId, title, publicKey) <> (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 +) diff --git a/src/main/scala/gitbucket/core/model/Profile.scala b/src/main/scala/gitbucket/core/model/Profile.scala index ad01a4c34..332e7ea30 100644 --- a/src/main/scala/gitbucket/core/model/Profile.scala +++ b/src/main/scala/gitbucket/core/model/Profile.scala @@ -54,5 +54,6 @@ trait CoreProfile extends ProfileProvider with Profile with WebHookComponent with WebHookEventComponent with ProtectedBranchComponent + with DeployKeyComponent object Profile extends CoreProfile From 5a1ec385a8566504ce217159f4fa5e5887ef781f Mon Sep 17 00:00:00 2001 From: Naoki Takezoe Date: Thu, 2 Mar 2017 17:31:04 +0900 Subject: [PATCH 2/6] (refs #474)Add controller to maintain deploy keys --- .../RepositorySettingsController.scala | 40 +++++++++++++----- .../core/service/DeployKeyService.scala | 22 ++++++++++ .../core/service/SshKeyService.scala | 4 +- .../gitbucket/core/account/ssh.scala.html | 2 +- .../core/settings/deploykey.scala.html | 42 +++++++++++++++++++ .../gitbucket/core/settings/menu.scala.html | 3 ++ 6 files changed, 100 insertions(+), 13 deletions(-) create mode 100644 src/main/scala/gitbucket/core/service/DeployKeyService.scala create mode 100644 src/main/twirl/gitbucket/core/settings/deploykey.scala.html diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index cd0371afc..303cd5669 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -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,14 @@ 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) + + val deployKeyForm = mapping( + "title" -> trim(label("Title", text(required, maxlength(100)))), + "publicKey" -> trim(label("Key" , text(required))) + )(DeployKeyForm.apply) // for web hook url addition case class WebHookForm(url: String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String]) @@ -382,6 +384,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) + 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. */ diff --git a/src/main/scala/gitbucket/core/service/DeployKeyService.scala b/src/main/scala/gitbucket/core/service/DeployKeyService.scala new file mode 100644 index 000000000..998c5725b --- /dev/null +++ b/src/main/scala/gitbucket/core/service/DeployKeyService.scala @@ -0,0 +1,22 @@ +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)(implicit s: Session): Unit = + DeployKeys.insert(DeployKey(userName = userName, repositoryName = repositoryName, title = title, publicKey = publicKey)) + + 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 + + +} diff --git a/src/main/scala/gitbucket/core/service/SshKeyService.scala b/src/main/scala/gitbucket/core/service/SshKeyService.scala index 477413fc9..5c7dc9d23 100644 --- a/src/main/scala/gitbucket/core/service/SshKeyService.scala +++ b/src/main/scala/gitbucket/core/service/SshKeyService.scala @@ -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 } diff --git a/src/main/twirl/gitbucket/core/account/ssh.scala.html b/src/main/twirl/gitbucket/core/account/ssh.scala.html index 2a87dfa1e..cccfdfe3f 100644 --- a/src/main/twirl/gitbucket/core/account/ssh.scala.html +++ b/src/main/twirl/gitbucket/core/account/ssh.scala.html @@ -20,7 +20,7 @@
-
Add an SSH Key
+
Add a SSH Key
diff --git a/src/main/twirl/gitbucket/core/settings/deploykey.scala.html b/src/main/twirl/gitbucket/core/settings/deploykey.scala.html new file mode 100644 index 000000000..7f5d7de7e --- /dev/null +++ b/src/main/twirl/gitbucket/core/settings/deploykey.scala.html @@ -0,0 +1,42 @@ +@(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){ +
+
Deploy keys
+
+ @if(deployKeys.isEmpty){ + No keys + } + @deployKeys.zipWithIndex.map { case (key, i) => + @if(i != 0){ +
+ } + @key.title (@SshUtil.fingerPrint(key.publicKey).getOrElse("Key is invalid.")) + Delete + } +
+
+ +
+
Add a deploy key
+
+
+ +
+ +
+
+ +
+ +
+ +
+
+ + } + } +} diff --git a/src/main/twirl/gitbucket/core/settings/menu.scala.html b/src/main/twirl/gitbucket/core/settings/menu.scala.html index 5a8895ce4..b81bcd31f 100644 --- a/src/main/twirl/gitbucket/core/settings/menu.scala.html +++ b/src/main/twirl/gitbucket/core/settings/menu.scala.html @@ -15,6 +15,9 @@ Service Hooks + + Deploy Keys + Danger Zone From 629aaa78d634001f2c72723403637d445ff31792 Mon Sep 17 00:00:00 2001 From: Naoki Takezoe Date: Thu, 2 Mar 2017 18:15:39 +0900 Subject: [PATCH 3/6] Code formatting --- .../gitbucket/core/service/SystemSettingsService.scala | 6 +++--- .../scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala index 0e1b67748..a04f69057 100644 --- a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala +++ b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala @@ -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]) diff --git a/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala b/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala index fa8d1ce8b..0b249e12b 100644 --- a/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala +++ b/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala @@ -14,14 +14,14 @@ object PublicKeyAuthenticator { // put in the ServerSession here to be read by GitCommand later private val userNameSessionKey = new AttributeStore.AttributeKey[String] - def putUserName(serverSession:ServerSession, userName:String):Unit = + def putUserName(serverSession: ServerSession, userName: String):Unit = serverSession.setAttribute(userNameSessionKey, userName) - def getUserName(serverSession:ServerSession):Option[String] = + def getUserName(serverSession: ServerSession):Option[String] = Option(serverSession.getAttribute(userNameSessionKey)) } -class PublicKeyAuthenticator(genericUser:String) extends PublickeyAuthenticator with SshKeyService { +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 = @@ -45,7 +45,7 @@ class PublicKeyAuthenticator(genericUser:String) extends PublickeyAuthenticator authenticated } - private def authenticateGenericUser(username: String, key: PublicKey, session: ServerSession, genericUser:String): Boolean = { + 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() From b5f287d75eccd9fa2127a0a24ec26840e1e0a752 Mon Sep 17 00:00:00 2001 From: Naoki Takezoe Date: Fri, 3 Mar 2017 10:47:21 +0900 Subject: [PATCH 4/6] (refs #474)Add authentication and authronization by deploy key --- .../scala/gitbucket/core/ssh/GitCommand.scala | 109 +++++++++------- .../core/ssh/PublicKeyAuthenticator.scala | 117 +++++++++++------- 2 files changed, 136 insertions(+), 90 deletions(-) diff --git a/src/main/scala/gitbucket/core/ssh/GitCommand.scala b/src/main/scala/gitbucket/core/ssh/GitCommand.scala index 9620dbd01..e3078e6c9 100644 --- a/src/main/scala/gitbucket/core/ssh/GitCommand.scala +++ b/src/main/scala/gitbucket/core/ssh/GitCommand.scala @@ -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 => diff --git a/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala b/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala index 0b249e12b..814e3e97e 100644 --- a/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala +++ b/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala @@ -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 + } } + } From 37d2a385176d7d956dbd52c63b7831cc6696412f Mon Sep 17 00:00:00 2001 From: Naoki Takezoe Date: Fri, 3 Mar 2017 11:13:30 +0900 Subject: [PATCH 5/6] =?UTF-8?q?(refs=20#474)Add=20=E2=80=9CAllow=20write?= =?UTF-8?q?=20access=E2=80=9D=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/update/gitbucket-core_4.11.xml | 1 + .../controller/RepositorySettingsController.scala | 9 +++++---- .../scala/gitbucket/core/model/DeployKey.scala | 6 ++++-- .../gitbucket/core/service/DeployKeyService.scala | 15 ++++++++++++--- .../twirl/gitbucket/core/account/ssh.scala.html | 2 +- .../gitbucket/core/settings/deploykey.scala.html | 10 +++++++++- 6 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/main/resources/update/gitbucket-core_4.11.xml b/src/main/resources/update/gitbucket-core_4.11.xml index 24f1f430b..1c41d8861 100644 --- a/src/main/resources/update/gitbucket-core_4.11.xml +++ b/src/main/resources/update/gitbucket-core_4.11.xml @@ -6,6 +6,7 @@ + diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index 303cd5669..b396e4be9 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -58,11 +58,12 @@ trait RepositorySettingsControllerBase extends ControllerBase { // for deploy key - case class DeployKeyForm(title: String, publicKey: String) + 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))) + "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 @@ -391,7 +392,7 @@ trait RepositorySettingsControllerBase extends ControllerBase { /** Register a deploy key */ post("/:owner/:repository/settings/deploykey", deployKeyForm)(ownerOnly { (form, repository) => - addDeployKey(repository.owner, repository.name, form.title, form.publicKey) + addDeployKey(repository.owner, repository.name, form.title, form.publicKey, form.allowWrite) redirect(s"/${repository.owner}/${repository.name}/settings/deploykey") }) diff --git a/src/main/scala/gitbucket/core/model/DeployKey.scala b/src/main/scala/gitbucket/core/model/DeployKey.scala index 829f25624..4f34e45a0 100644 --- a/src/main/scala/gitbucket/core/model/DeployKey.scala +++ b/src/main/scala/gitbucket/core/model/DeployKey.scala @@ -11,7 +11,8 @@ trait DeployKeyComponent { self: Profile => val deployKeyId = column[Int]("DEPLOY_KEY_ID", O AutoInc) val title = column[String]("TITLE") val publicKey = column[String]("PUBLIC_KEY") - def * = (userName, repositoryName, deployKeyId, title, publicKey) <> (DeployKey.tupled, DeployKey.unapply) + 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) @@ -23,5 +24,6 @@ case class DeployKey( repositoryName: String, deployKeyId: Int = 0, title: String, - publicKey: String + publicKey: String, + allowWrite: Boolean ) diff --git a/src/main/scala/gitbucket/core/service/DeployKeyService.scala b/src/main/scala/gitbucket/core/service/DeployKeyService.scala index 998c5725b..7313bc614 100644 --- a/src/main/scala/gitbucket/core/service/DeployKeyService.scala +++ b/src/main/scala/gitbucket/core/service/DeployKeyService.scala @@ -6,11 +6,20 @@ import gitbucket.core.model.Profile.profile.blockingApi._ trait DeployKeyService { - def addDeployKey(userName: String, repositoryName: String, title: String, publicKey: String)(implicit s: Session): Unit = - DeployKeys.insert(DeployKey(userName = userName, repositoryName = repositoryName, title = title, publicKey = publicKey)) + 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 + 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 diff --git a/src/main/twirl/gitbucket/core/account/ssh.scala.html b/src/main/twirl/gitbucket/core/account/ssh.scala.html index cccfdfe3f..a8fe8122b 100644 --- a/src/main/twirl/gitbucket/core/account/ssh.scala.html +++ b/src/main/twirl/gitbucket/core/account/ssh.scala.html @@ -30,7 +30,7 @@
- +
diff --git a/src/main/twirl/gitbucket/core/settings/deploykey.scala.html b/src/main/twirl/gitbucket/core/settings/deploykey.scala.html index 7f5d7de7e..740ec20bd 100644 --- a/src/main/twirl/gitbucket/core/settings/deploykey.scala.html +++ b/src/main/twirl/gitbucket/core/settings/deploykey.scala.html @@ -15,6 +15,9 @@
} @key.title (@SshUtil.fingerPrint(key.publicKey).getOrElse("Key is invalid.")) + @if(key.allowWrite){ + + } Delete }
@@ -31,7 +34,12 @@
- + +
+
+
From bc69a67b0565b5c03c1cd396759232acb6d17207 Mon Sep 17 00:00:00 2001 From: Naoki Takezoe Date: Fri, 3 Mar 2017 11:48:08 +0900 Subject: [PATCH 6/6] (refs #474)Fix authorization for cloning repository Allows cloning a repository for users who can read access to that repository. --- .../scala/gitbucket/core/ssh/GitCommand.scala | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/scala/gitbucket/core/ssh/GitCommand.scala b/src/main/scala/gitbucket/core/ssh/GitCommand.scala index e3078e6c9..ca1e0213e 100644 --- a/src/main/scala/gitbucket/core/ssh/GitCommand.scala +++ b/src/main/scala/gitbucket/core/ssh/GitCommand.scala @@ -95,6 +95,24 @@ abstract class DefaultGitCommand(val owner: String, val repoName: String) extend } } + 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 { @@ -106,7 +124,7 @@ abstract class DefaultGitCommand(val owner: String, val repoName: String) extend } case AuthType.DeployKeyType(key) => { getDeployKeys(owner, repoName).filter(sshKey => SshUtil.str2PublicKey(sshKey.publicKey).exists(_ == key)) match { - case List(_) => true + case List(x) if x.allowWrite => true case _ => false } } @@ -122,7 +140,7 @@ class DefaultGitUploadPack(owner: String, repoName: String) extends DefaultGitCo 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(authType, repositoryInfo) + !repositoryInfo.repository.isPrivate || isReadableUser(authType, repositoryInfo) }.getOrElse(false) }