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 0000000..1c41d88 --- /dev/null +++ b/src/main/resources/update/gitbucket-core_4.11.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala index 4b9c486..dd8665d 100644 --- a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala +++ b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala @@ -28,5 +28,8 @@ 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/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index cd0371a..b396e4b 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -2,7 +2,7 @@ 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 @@ 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 @@ 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 @@ "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 @@ 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. */ 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 0000000..4f34e45 --- /dev/null +++ b/src/main/scala/gitbucket/core/model/DeployKey.scala @@ -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 +) diff --git a/src/main/scala/gitbucket/core/model/Profile.scala b/src/main/scala/gitbucket/core/model/Profile.scala index ad01a4c..332e7ea 100644 --- a/src/main/scala/gitbucket/core/model/Profile.scala +++ b/src/main/scala/gitbucket/core/model/Profile.scala @@ -54,5 +54,6 @@ with WebHookComponent with WebHookEventComponent with ProtectedBranchComponent + with DeployKeyComponent object Profile extends CoreProfile 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 0000000..7313bc6 --- /dev/null +++ b/src/main/scala/gitbucket/core/service/DeployKeyService.scala @@ -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 + + +} diff --git a/src/main/scala/gitbucket/core/service/SshKeyService.scala b/src/main/scala/gitbucket/core/service/SshKeyService.scala index 477413f..5c7dc9d 100644 --- a/src/main/scala/gitbucket/core/service/SshKeyService.scala +++ b/src/main/scala/gitbucket/core/service/SshKeyService.scala @@ -7,7 +7,7 @@ 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 @@ 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/scala/gitbucket/core/service/SystemSettingsService.scala b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala index 0e1b677..a04f690 100644 --- a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala +++ b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala @@ -175,9 +175,9 @@ 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/GitCommand.scala b/src/main/scala/gitbucket/core/ssh/GitCommand.scala index 9620dbd..ca1e021 100644 --- a/src/main/scala/gitbucket/core/ssh/GitCommand.scala +++ b/src/main/scala/gitbucket/core/ssh/GitCommand.scala @@ -2,7 +2,7 @@ import gitbucket.core.model.Profile.profile.blockingApi._ import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry} -import gitbucket.core.service.{AccountService, RepositoryService, SystemSettingsService} +import gitbucket.core.service.{AccountService, DeployKeyService, RepositoryService, SystemSettingsService} import gitbucket.core.servlet.{CommitLogHook, Database} import gitbucket.core.util.{ControlUtil, Directory} import org.apache.sshd.server.{Command, CommandFactory, Environment, ExitCallback, SessionAware} @@ -13,6 +13,7 @@ import ControlUtil._ import org.eclipse.jgit.api.Git import Directory._ +import gitbucket.core.ssh.PublicKeyAuthenticator.AuthType import org.eclipse.jgit.transport.{ReceivePack, UploadPack} import org.apache.sshd.server.scp.UnknownCommand import org.eclipse.jgit.errors.RepositoryNotFoundException @@ -25,34 +26,33 @@ abstract class GitCommand extends Command with SessionAware { private val logger = LoggerFactory.getLogger(classOf[GitCommand]) + @volatile protected var err: OutputStream = null @volatile protected var in: InputStream = null @volatile protected var out: OutputStream = null @volatile protected var callback: ExitCallback = null - @volatile private var authUser:Option[String] = None + @volatile private var authType: Option[AuthType] = None - protected def runTask(authUser: String): Unit + protected def runTask(authType: AuthType): Unit - private def newTask(): Runnable = new Runnable { - override def run(): Unit = { - authUser match { - case Some(authUser) => - try { - runTask(authUser) - callback.onExit(0) - } catch { - case e: RepositoryNotFoundException => - logger.info(e.getMessage) - callback.onExit(1, "Repository Not Found") - case e: Throwable => - logger.error(e.getMessage, e) - callback.onExit(1) - } - case None => - val message = "User not authenticated" - logger.error(message) - callback.onExit(1, message) - } + private def newTask(): Runnable = () => { + authType match { + case Some(authType) => + try { + runTask(authType) + callback.onExit(0) + } catch { + case e: RepositoryNotFoundException => + logger.info(e.getMessage) + callback.onExit(1, "Repository Not Found") + case e: Throwable => + logger.error(e.getMessage, e) + callback.onExit(1) + } + case None => + val message = "User not authenticated" + logger.error(message) + callback.onExit(1, message) } } @@ -79,32 +79,68 @@ 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 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 @@ 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 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 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 fa8d1ce..814e3e9 100644 --- a/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala +++ b/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala @@ -2,73 +2,102 @@ import java.security.PublicKey -import gitbucket.core.service.SshKeyService +import gitbucket.core.service.{DeployKeyService, SshKeyService} import gitbucket.core.servlet.Database import gitbucket.core.model.Profile.profile.blockingApi._ +import gitbucket.core.ssh.PublicKeyAuthenticator.AuthType import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator import org.apache.sshd.server.session.ServerSession import org.apache.sshd.common.AttributeStore import org.slf4j.LoggerFactory object PublicKeyAuthenticator { + // put in the ServerSession here to be read by GitCommand later - private val userNameSessionKey = new AttributeStore.AttributeKey[String] + private val authTypeSessionKey = new AttributeStore.AttributeKey[AuthType] - def putUserName(serverSession:ServerSession, userName:String):Unit = - serverSession.setAttribute(userNameSessionKey, userName) + def putAuthType(serverSession: ServerSession, authType: AuthType):Unit = + serverSession.setAttribute(authTypeSessionKey, authType) - def getUserName(serverSession:ServerSession):Option[String] = - Option(serverSession.getAttribute(userNameSessionKey)) + def getAuthType(serverSession: ServerSession): Option[AuthType] = + Option(serverSession.getAttribute(authTypeSessionKey)) + + sealed trait AuthType + + object AuthType { + case class UserAuthType(userName: String) extends AuthType + case class DeployKeyType(publicKey: PublicKey) extends AuthType + + /** + * Retrieves username if authType is UserAuthType, otherwise None. + */ + def userName(authType: AuthType): Option[String] = { + authType match { + case UserAuthType(userName) => Some(userName) + case _ => None + } + } + } } -class PublicKeyAuthenticator(genericUser:String) extends PublickeyAuthenticator with SshKeyService { +class PublicKeyAuthenticator(genericUser: String) extends PublickeyAuthenticator with SshKeyService with DeployKeyService { private val logger = LoggerFactory.getLogger(classOf[PublicKeyAuthenticator]) - override def authenticate(username: String, key: PublicKey, session: ServerSession): Boolean = - if (username == genericUser) authenticateGenericUser(username, key, session, genericUser) - else authenticateLoginUser(username, key, session) - - private def authenticateLoginUser(username: String, key: PublicKey, session: ServerSession): Boolean = { - val authenticated = - Database() - .withSession { implicit dbSession => getPublicKeys(username) } - .map(_.publicKey) - .flatMap(SshUtil.str2PublicKey) - .contains(key) - if (authenticated) { - logger.info(s"authentication as ssh user ${username} succeeded") - PublicKeyAuthenticator.putUserName(session, username) + override def authenticate(username: String, key: PublicKey, session: ServerSession): Boolean = { + Database().withSession { implicit s => + if (username == genericUser) { + authenticateGenericUser(username, key, session, genericUser) + } else { + authenticateLoginUser(username, key, session) + } } - else { - logger.info(s"authentication as ssh user ${username} failed") + } + + private def authenticateLoginUser(userName: String, key: PublicKey, session: ServerSession)(implicit s: Session): Boolean = { + val authenticated = getPublicKeys(userName).map(_.publicKey).flatMap(SshUtil.str2PublicKey).contains(key) + + if (authenticated) { + logger.info(s"authentication as ssh user ${userName} succeeded") + PublicKeyAuthenticator.putAuthType(session, AuthType.UserAuthType(userName)) + } else { + logger.info(s"authentication as ssh user ${userName} failed") } authenticated } - private def authenticateGenericUser(username: String, key: PublicKey, session: ServerSession, genericUser:String): Boolean = { + private def authenticateGenericUser(userName: String, key: PublicKey, session: ServerSession, genericUser: String)(implicit s: Session): Boolean = { // find all users having the key we got from ssh - val possibleUserNames = - Database() - .withSession { implicit dbSession => getAllKeys() } - .filter { sshKey => + val possibleUserNames = getAllKeys().filter { sshKey => + SshUtil.str2PublicKey(sshKey.publicKey).exists(_ == key) + }.map(_.userName).distinct + + // determine the user - if different accounts share the same key, tough luck + val uniqueUserName = possibleUserNames match { + case List(name) => Some(name) + case _ => None + } + + uniqueUserName.map { userName => + // found public key for user + logger.info(s"authentication as generic user ${genericUser} succeeded, identified ${userName}") + PublicKeyAuthenticator.putAuthType(session, AuthType.UserAuthType(userName)) + true + }.getOrElse { + // search deploy keys + val existsDeployKey = getAllDeployKeys().exists { sshKey => SshUtil.str2PublicKey(sshKey.publicKey).exists(_ == key) } - .map(_.userName) - .distinct - // determine the user - if different accounts share the same key, tough luck - val uniqueUserName = - possibleUserNames match { - case List() => - logger.info(s"authentication as generic user ${genericUser} failed, public key not found") - None - case List(name) => - logger.info(s"authentication as generic user ${genericUser} succeeded, identified ${name}") - Some(name) - case _ => - logger.info(s"authentication as generic user ${genericUser} failed, public key is ambiguous") - None + if(existsDeployKey){ + // found deploy key for repository + PublicKeyAuthenticator.putAuthType(session, AuthType.DeployKeyType(key)) + logger.info(s"authentication as generic user ${genericUser} succeeded, deploy key was found") + true + } else { + // public key not found + logger.info(s"authentication by generic user ${genericUser} failed") + false } - uniqueUserName.foreach(PublicKeyAuthenticator.putUserName(session, _)) - uniqueUserName.isDefined + } } + } diff --git a/src/main/twirl/gitbucket/core/account/ssh.scala.html b/src/main/twirl/gitbucket/core/account/ssh.scala.html index 2a87dfa..a8fe812 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
@@ -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 new file mode 100644 index 0000000..740ec20 --- /dev/null +++ b/src/main/twirl/gitbucket/core/settings/deploykey.scala.html @@ -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){ +
+
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.")) + @if(key.allowWrite){ + + } + 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 5a8895c..b81bcd3 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