mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-06 13:35:50 +01:00
Verify gpg sign (#2264)
This commit is contained in:
committed by
Naoki Takezoe
parent
33277bf25f
commit
8705d3450a
16
src/main/resources/update/gitbucket-core_4.31.xml
Normal file
16
src/main/resources/update/gitbucket-core_4.31.xml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<changeSet>
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- SSH_KEY -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="GPG_KEY">
|
||||||
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="KEY_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
|
||||||
|
<column name="GPG_KEY_ID" type="bigint" nullable="false"/>
|
||||||
|
<column name="TITLE" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="PUBLIC_KEY" type="text" nullable="false"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_GPG_KEY_PK" tableName="GPG_KEY" columnNames="USER_NAME, GPG_KEY_ID"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_GPG_KEY_FK0" baseTableName="GPG_KEY" baseColumnNames="USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
|
||||||
|
</changeSet>
|
||||||
@@ -60,5 +60,6 @@ object GitBucketCoreModule
|
|||||||
new Version("4.28.0"),
|
new Version("4.28.0"),
|
||||||
new Version("4.29.0"),
|
new Version("4.29.0"),
|
||||||
new Version("4.30.0"),
|
new Version("4.30.0"),
|
||||||
new Version("4.30.1")
|
new Version("4.30.1"),
|
||||||
|
new Version("4.31.0", new LiquibaseMigration("update/gitbucket-core_4.31.xml"))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class AccountController
|
|||||||
with WikiService
|
with WikiService
|
||||||
with LabelsService
|
with LabelsService
|
||||||
with SshKeyService
|
with SshKeyService
|
||||||
|
with GpgKeyService
|
||||||
with OneselfAuthenticator
|
with OneselfAuthenticator
|
||||||
with UsersAuthenticator
|
with UsersAuthenticator
|
||||||
with GroupManagerAuthenticator
|
with GroupManagerAuthenticator
|
||||||
@@ -42,6 +43,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
with WikiService
|
with WikiService
|
||||||
with LabelsService
|
with LabelsService
|
||||||
with SshKeyService
|
with SshKeyService
|
||||||
|
with GpgKeyService
|
||||||
with OneselfAuthenticator
|
with OneselfAuthenticator
|
||||||
with UsersAuthenticator
|
with UsersAuthenticator
|
||||||
with GroupManagerAuthenticator
|
with GroupManagerAuthenticator
|
||||||
@@ -75,6 +77,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
|
|
||||||
case class SshKeyForm(title: String, publicKey: String)
|
case class SshKeyForm(title: String, publicKey: String)
|
||||||
|
|
||||||
|
case class GpgKeyForm(title: String, publicKey: String)
|
||||||
|
|
||||||
case class PersonalTokenForm(note: String)
|
case class PersonalTokenForm(note: String)
|
||||||
|
|
||||||
val newForm = mapping(
|
val newForm = mapping(
|
||||||
@@ -108,6 +112,11 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
"publicKey" -> trim2(label("Key", text(required, validPublicKey)))
|
"publicKey" -> trim2(label("Key", text(required, validPublicKey)))
|
||||||
)(SshKeyForm.apply)
|
)(SshKeyForm.apply)
|
||||||
|
|
||||||
|
val gpgKeyForm = mapping(
|
||||||
|
"title" -> trim(label("Title", text(required, maxlength(100)))),
|
||||||
|
"publicKey" -> label("Key", text(required, validGpgPublicKey))
|
||||||
|
)(GpgKeyForm.apply)
|
||||||
|
|
||||||
val personalTokenForm = mapping(
|
val personalTokenForm = mapping(
|
||||||
"note" -> trim(label("Token", text(required, maxlength(100))))
|
"note" -> trim(label("Token", text(required, maxlength(100))))
|
||||||
)(PersonalTokenForm.apply)
|
)(PersonalTokenForm.apply)
|
||||||
@@ -387,6 +396,27 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
redirect(s"/${userName}/_ssh")
|
redirect(s"/${userName}/_ssh")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
get("/:userName/_gpg")(oneselfOnly {
|
||||||
|
val userName = params("userName")
|
||||||
|
getAccountByUserName(userName).map { x =>
|
||||||
|
//html.ssh(x, getPublicKeys(x.userName))
|
||||||
|
html.gpg(x, getGpgPublicKeys(x.userName))
|
||||||
|
} getOrElse NotFound()
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/:userName/_gpg", gpgKeyForm)(oneselfOnly { form =>
|
||||||
|
val userName = params("userName")
|
||||||
|
addGpgPublicKey(userName, form.title, form.publicKey)
|
||||||
|
redirect(s"/${userName}/_gpg")
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:userName/_gpg/delete/:id")(oneselfOnly {
|
||||||
|
val userName = params("userName")
|
||||||
|
val keyId = params("id").toInt
|
||||||
|
deleteGpgPublicKey(userName, keyId)
|
||||||
|
redirect(s"/${userName}/_gpg")
|
||||||
|
})
|
||||||
|
|
||||||
get("/:userName/_application")(oneselfOnly {
|
get("/:userName/_application")(oneselfOnly {
|
||||||
val userName = params("userName")
|
val userName = params("userName")
|
||||||
getAccountByUserName(userName).map { x =>
|
getAccountByUserName(userName).map { x =>
|
||||||
@@ -771,6 +801,20 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private def validGpgPublicKey: Constraint = new Constraint() {
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||||
|
GpgUtil.str2GpgKeyId(value) match {
|
||||||
|
case Some(s) if GpgUtil.getGpgKey(s).isEmpty =>
|
||||||
|
None
|
||||||
|
case Some(_) =>
|
||||||
|
Some("GPG key is duplicated.")
|
||||||
|
case None =>
|
||||||
|
Some("GPG key is invalid.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private def validAccountName: Constraint = new Constraint() {
|
private def validAccountName: Constraint = new Constraint() {
|
||||||
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||||
getAccountByUserName(value) match {
|
getAccountByUserName(value) match {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import gitbucket.core.util.SyntaxSugars._
|
|||||||
import gitbucket.core.util.Implicits._
|
import gitbucket.core.util.Implicits._
|
||||||
import gitbucket.core.util.Directory._
|
import gitbucket.core.util.Directory._
|
||||||
import gitbucket.core.model.{Account, CommitState, CommitStatus}
|
import gitbucket.core.model.{Account, CommitState, CommitStatus}
|
||||||
|
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||||
import gitbucket.core.view
|
import gitbucket.core.view
|
||||||
import gitbucket.core.view.helpers
|
import gitbucket.core.view.helpers
|
||||||
import org.apache.commons.compress.archivers.{ArchiveEntry, ArchiveOutputStream}
|
import org.apache.commons.compress.archivers.{ArchiveEntry, ArchiveOutputStream}
|
||||||
@@ -273,9 +274,30 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
if (path.isEmpty) Nil else path.split("/").toList,
|
if (path.isEmpty) Nil else path.split("/").toList,
|
||||||
branchName,
|
branchName,
|
||||||
repository,
|
repository,
|
||||||
logs.splitWith { (commit1, commit2) =>
|
logs
|
||||||
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
|
.map {
|
||||||
},
|
c =>
|
||||||
|
CommitInfo(
|
||||||
|
id = c.id,
|
||||||
|
shortMessage = c.shortMessage,
|
||||||
|
fullMessage = c.fullMessage,
|
||||||
|
parents = c.parents,
|
||||||
|
authorTime = c.authorTime,
|
||||||
|
authorName = c.authorName,
|
||||||
|
authorEmailAddress = c.authorEmailAddress,
|
||||||
|
commitTime = c.commitTime,
|
||||||
|
committerName = c.committerName,
|
||||||
|
committerEmailAddress = c.committerEmailAddress,
|
||||||
|
commitSign = c.commitSign,
|
||||||
|
verified = c.commitSign
|
||||||
|
.flatMap { s =>
|
||||||
|
GpgUtil.verifySign(s)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.splitWith { (commit1, commit2) =>
|
||||||
|
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
|
||||||
|
},
|
||||||
page,
|
page,
|
||||||
hasNext,
|
hasNext,
|
||||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount),
|
hasDeveloperRole(repository.owner, repository.name, context.loginAccount),
|
||||||
|
|||||||
29
src/main/scala/gitbucket/core/model/GpgKey.scala
Normal file
29
src/main/scala/gitbucket/core/model/GpgKey.scala
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package gitbucket.core.model
|
||||||
|
|
||||||
|
trait GpgKeyComponent { self: Profile =>
|
||||||
|
import profile.api._
|
||||||
|
|
||||||
|
lazy val GpgKeys = TableQuery[GpgKeys]
|
||||||
|
|
||||||
|
class GpgKeys(tag: Tag) extends Table[GpgKey](tag, "GPG_KEY") {
|
||||||
|
val userName = column[String]("USER_NAME")
|
||||||
|
val keyId = column[Int]("KEY_ID", O AutoInc)
|
||||||
|
val gpgKeyId = column[Long]("GPG_KEY_ID")
|
||||||
|
val title = column[String]("TITLE")
|
||||||
|
val publicKey = column[String]("PUBLIC_KEY")
|
||||||
|
def * = (userName, keyId, gpgKeyId, title, publicKey) <> (GpgKey.tupled, GpgKey.unapply)
|
||||||
|
|
||||||
|
def byPrimaryKey(userName: String, keyId: Int) =
|
||||||
|
(this.userName === userName.bind) && (this.keyId === keyId.bind)
|
||||||
|
def byGpgKeyId(gpgKeyId: Long) =
|
||||||
|
this.gpgKeyId === gpgKeyId.bind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class GpgKey(
|
||||||
|
userName: String,
|
||||||
|
keyId: Int = 0,
|
||||||
|
gpgKeyId: Long,
|
||||||
|
title: String,
|
||||||
|
publicKey: String
|
||||||
|
)
|
||||||
@@ -59,6 +59,7 @@ trait CoreProfile
|
|||||||
with PullRequestComponent
|
with PullRequestComponent
|
||||||
with RepositoryComponent
|
with RepositoryComponent
|
||||||
with SshKeyComponent
|
with SshKeyComponent
|
||||||
|
with GpgKeyComponent
|
||||||
with RepositoryWebHookComponent
|
with RepositoryWebHookComponent
|
||||||
with RepositoryWebHookEventComponent
|
with RepositoryWebHookEventComponent
|
||||||
with AccountWebHookComponent
|
with AccountWebHookComponent
|
||||||
|
|||||||
29
src/main/scala/gitbucket/core/service/GpgKeyService.scala
Normal file
29
src/main/scala/gitbucket/core/service/GpgKeyService.scala
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package gitbucket.core.service
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
|
||||||
|
import gitbucket.core.model.GpgKey
|
||||||
|
|
||||||
|
import collection.JavaConverters._
|
||||||
|
import gitbucket.core.model.Profile._
|
||||||
|
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||||
|
import org.bouncycastle.bcpg.ArmoredInputStream
|
||||||
|
import org.bouncycastle.openpgp.PGPPublicKeyRing
|
||||||
|
import org.bouncycastle.openpgp.bc.BcPGPObjectFactory
|
||||||
|
|
||||||
|
trait GpgKeyService {
|
||||||
|
def getGpgPublicKeys(userName: String)(implicit s: Session): List[GpgKey] =
|
||||||
|
GpgKeys.filter(_.userName === userName.bind).sortBy(_.gpgKeyId).list
|
||||||
|
|
||||||
|
def addGpgPublicKey(userName: String, title: String, publicKey: String)(implicit s: Session): Unit = {
|
||||||
|
val pubKeyOf = new BcPGPObjectFactory(new ArmoredInputStream(new ByteArrayInputStream(publicKey.getBytes)))
|
||||||
|
pubKeyOf.iterator().asScala.foreach {
|
||||||
|
case keyRing: PGPPublicKeyRing =>
|
||||||
|
val key = keyRing.getPublicKey()
|
||||||
|
GpgKeys.insert(GpgKey(userName = userName, gpgKeyId = key.getKeyID, title = title, publicKey = publicKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def deleteGpgPublicKey(userName: String, keyId: Int)(implicit s: Session): Unit =
|
||||||
|
GpgKeys.filter(_.byPrimaryKey(userName, keyId)).delete
|
||||||
|
}
|
||||||
61
src/main/scala/gitbucket/core/util/GpgUtil.scala
Normal file
61
src/main/scala/gitbucket/core/util/GpgUtil.scala
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package gitbucket.core.util
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
|
||||||
|
import collection.JavaConverters._
|
||||||
|
import gitbucket.core.model.Profile._
|
||||||
|
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||||
|
import org.bouncycastle.bcpg.ArmoredInputStream
|
||||||
|
import org.bouncycastle.openpgp.{PGPPublicKey, PGPPublicKeyRing, PGPSignatureList}
|
||||||
|
import org.bouncycastle.openpgp.bc.BcPGPObjectFactory
|
||||||
|
import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider
|
||||||
|
|
||||||
|
object GpgUtil {
|
||||||
|
def str2GpgKeyId(keyStr: String): Option[Long] = {
|
||||||
|
val pubKeyOf = new BcPGPObjectFactory(new ArmoredInputStream(new ByteArrayInputStream(keyStr.getBytes)))
|
||||||
|
pubKeyOf.iterator().asScala.collectFirst {
|
||||||
|
case keyRing: PGPPublicKeyRing =>
|
||||||
|
keyRing.getPublicKey().getKeyID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getGpgKey(gpgKeyId: Long)(implicit s: Session): Option[PGPPublicKey] = {
|
||||||
|
val pubKeyOpt = GpgKeys.filter(_.byGpgKeyId(gpgKeyId)).map { _.publicKey }.firstOption
|
||||||
|
pubKeyOpt.flatMap { pubKeyStr =>
|
||||||
|
val pubKeyObjFactory =
|
||||||
|
new BcPGPObjectFactory(new ArmoredInputStream(new ByteArrayInputStream(pubKeyStr.getBytes())))
|
||||||
|
pubKeyObjFactory.nextObject() match {
|
||||||
|
case pubKeyRing: PGPPublicKeyRing =>
|
||||||
|
Option(pubKeyRing.getPublicKey(gpgKeyId))
|
||||||
|
case _ =>
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def verifySign(signInfo: JGitUtil.GpgSignInfo)(implicit s: Session): Option[JGitUtil.GpgVerifyInfo] = {
|
||||||
|
new BcPGPObjectFactory(new ArmoredInputStream(new ByteArrayInputStream(signInfo.signArmored)))
|
||||||
|
.iterator()
|
||||||
|
.asScala
|
||||||
|
.flatMap {
|
||||||
|
case signList: PGPSignatureList =>
|
||||||
|
signList
|
||||||
|
.iterator()
|
||||||
|
.asScala
|
||||||
|
.flatMap { sign =>
|
||||||
|
getGpgKey(sign.getKeyID)
|
||||||
|
.map { pubKey =>
|
||||||
|
sign.init(new BcPGPContentVerifierBuilderProvider, pubKey)
|
||||||
|
sign.update(signInfo.target)
|
||||||
|
(sign, pubKey)
|
||||||
|
}
|
||||||
|
.collect {
|
||||||
|
case (sign, pubKey) if sign.verify() =>
|
||||||
|
JGitUtil.GpgVerifyInfo(pubKey.getUserIDs.next, pubKey.getKeyID.toHexString.toUpperCase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.toList
|
||||||
|
.headOption
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package gitbucket.core.util
|
package gitbucket.core.util
|
||||||
|
|
||||||
import java.io.{ByteArrayOutputStream, File, FileInputStream, InputStream}
|
import java.io._
|
||||||
|
|
||||||
import gitbucket.core.service.RepositoryService
|
import gitbucket.core.service.RepositoryService
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
@@ -75,6 +75,59 @@ object JGitUtil {
|
|||||||
linkUrl: Option[String]
|
linkUrl: Option[String]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The gpg commit sign data.
|
||||||
|
* @param signArmored signature for commit
|
||||||
|
* @param target string for verification target
|
||||||
|
*/
|
||||||
|
case class GpgSignInfo(signArmored: Array[Byte], target: Array[Byte])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The verified gpg sign data.
|
||||||
|
* @param signedUser
|
||||||
|
* @param signedKeyId
|
||||||
|
*/
|
||||||
|
case class GpgVerifyInfo(signedUser: String, signedKeyId: String)
|
||||||
|
|
||||||
|
private def getSignTarget(rev: RevCommit): Array[Byte] = {
|
||||||
|
val ascii = "ASCII"
|
||||||
|
val os = new ByteArrayOutputStream()
|
||||||
|
val w = new OutputStreamWriter(os, rev.getEncoding)
|
||||||
|
os.write("tree ".getBytes(ascii))
|
||||||
|
rev.getTree.copyTo(os)
|
||||||
|
os.write('\n')
|
||||||
|
|
||||||
|
rev.getParents.foreach { p =>
|
||||||
|
os.write("parent ".getBytes(ascii))
|
||||||
|
p.copyTo(os)
|
||||||
|
os.write('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
os.write("author ".getBytes(ascii))
|
||||||
|
w.write(rev.getAuthorIdent.toExternalString)
|
||||||
|
w.flush()
|
||||||
|
os.write('\n')
|
||||||
|
|
||||||
|
os.write("committer ".getBytes(ascii))
|
||||||
|
w.write(rev.getCommitterIdent.toExternalString)
|
||||||
|
w.flush()
|
||||||
|
os.write('\n')
|
||||||
|
|
||||||
|
if (rev.getEncoding.name != "UTF-8") {
|
||||||
|
os.write("encoding ".getBytes(ascii))
|
||||||
|
os.write(Constants.encodeASCII(rev.getEncoding.name))
|
||||||
|
os.write('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
os.write('\n')
|
||||||
|
|
||||||
|
if (!rev.getFullMessage.isEmpty) {
|
||||||
|
w.write(rev.getFullMessage)
|
||||||
|
w.flush()
|
||||||
|
}
|
||||||
|
os.toByteArray
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The commit data.
|
* The commit data.
|
||||||
*
|
*
|
||||||
@@ -99,7 +152,9 @@ object JGitUtil {
|
|||||||
authorEmailAddress: String,
|
authorEmailAddress: String,
|
||||||
commitTime: Date,
|
commitTime: Date,
|
||||||
committerName: String,
|
committerName: String,
|
||||||
committerEmailAddress: String
|
committerEmailAddress: String,
|
||||||
|
commitSign: Option[GpgSignInfo],
|
||||||
|
verified: Option[GpgVerifyInfo]
|
||||||
) {
|
) {
|
||||||
|
|
||||||
def this(rev: org.eclipse.jgit.revwalk.RevCommit) =
|
def this(rev: org.eclipse.jgit.revwalk.RevCommit) =
|
||||||
@@ -113,7 +168,11 @@ object JGitUtil {
|
|||||||
rev.getAuthorIdent.getEmailAddress,
|
rev.getAuthorIdent.getEmailAddress,
|
||||||
rev.getCommitterIdent.getWhen,
|
rev.getCommitterIdent.getWhen,
|
||||||
rev.getCommitterIdent.getName,
|
rev.getCommitterIdent.getName,
|
||||||
rev.getCommitterIdent.getEmailAddress
|
rev.getCommitterIdent.getEmailAddress,
|
||||||
|
Option(rev.getRawGpgSignature).map { s =>
|
||||||
|
GpgSignInfo(s, getSignTarget(rev))
|
||||||
|
},
|
||||||
|
None
|
||||||
)
|
)
|
||||||
|
|
||||||
val summary = getSummaryMessage(fullMessage, shortMessage)
|
val summary = getSummaryMessage(fullMessage, shortMessage)
|
||||||
|
|||||||
39
src/main/twirl/gitbucket/core/account/gpg.scala.html
Normal file
39
src/main/twirl/gitbucket/core/account/gpg.scala.html
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
@(account: gitbucket.core.model.Account, gpgKeys: List[gitbucket.core.model.GpgKey])(implicit context: gitbucket.core.controller.Context)
|
||||||
|
@import gitbucket.core.ssh.SshUtil
|
||||||
|
@gitbucket.core.html.main("GPG Keys"){
|
||||||
|
@gitbucket.core.account.html.menu("gpg", context.loginAccount.get.userName, false){
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading strong">GPG Keys</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
@if(gpgKeys.isEmpty){
|
||||||
|
No keys
|
||||||
|
}
|
||||||
|
@gpgKeys.zipWithIndex.map { case (key, i) =>
|
||||||
|
@if(i != 0){
|
||||||
|
<hr>
|
||||||
|
}
|
||||||
|
<strong style="line-height: 30px;">@key.title</strong> (@key.gpgKeyId.toHexString.toUpperCase)
|
||||||
|
<a href="@context.path/@account.userName/_gpg/delete/@key.keyId" class="btn btn-sm btn-danger pull-right">Delete</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="@context.path/@account.userName/_gpg" validate="true" autocomplete="off">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading strong">Add a GPG 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>
|
||||||
|
<input type="submit" class="btn btn-success" value="Add"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,11 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
|
<li class="menu-item-hover @if(active=="gpg"){active}">
|
||||||
|
<a href="@context.path/@userName/_gpg">
|
||||||
|
<i class="menu-icon octicon octicon-key"></i> <span>GPG Keys</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="menu-item-hover @if(active=="application"){active}">
|
<li class="menu-item-hover @if(active=="application"){active}">
|
||||||
<a href="@context.path/@userName/_application">
|
<a href="@context.path/@userName/_application">
|
||||||
<i class="menu-icon octicon octicon-rocket"></i> <span>Applications</span>
|
<i class="menu-icon octicon octicon-rocket"></i> <span>Applications</span>
|
||||||
|
|||||||
@@ -40,6 +40,13 @@
|
|||||||
@if(i != 0){ <tr> }
|
@if(i != 0){ <tr> }
|
||||||
<td>
|
<td>
|
||||||
<div class="pull-right text-right">
|
<div class="pull-right text-right">
|
||||||
|
@if(commit.commitSign.isDefined){
|
||||||
|
@commit.verified.map{ v =>
|
||||||
|
<span class="gpg-verified">Verified signed by @v.signedUser (@v.signedKeyId)</span>
|
||||||
|
}.getOrElse{
|
||||||
|
<span class="gpg-unverified">Unverified</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
@defining(getTags(commit.id)) { tags =>
|
@defining(getTags(commit.id)) { tags =>
|
||||||
@if(tags.nonEmpty){
|
@if(tags.nonEmpty){
|
||||||
<span class="muted">
|
<span class="muted">
|
||||||
|
|||||||
@@ -250,7 +250,9 @@ object ApiSpecModels {
|
|||||||
authorEmailAddress = account.mailAddress,
|
authorEmailAddress = account.mailAddress,
|
||||||
commitTime = date1,
|
commitTime = date1,
|
||||||
committerName = account.userName,
|
committerName = account.userName,
|
||||||
committerEmailAddress = account.mailAddress
|
committerEmailAddress = account.mailAddress,
|
||||||
|
None,
|
||||||
|
None
|
||||||
)
|
)
|
||||||
|
|
||||||
val apiCommitListItem = ApiCommitListItem(
|
val apiCommitListItem = ApiCommitListItem(
|
||||||
|
|||||||
Reference in New Issue
Block a user