Fix issue #884 - Support other UIDs from GPG key for key signature

verification
This commit is contained in:
Robin Shen 2022-08-22 22:41:29 +08:00
parent 3e24db0dec
commit c9262b6864
21 changed files with 183 additions and 109 deletions

View File

@ -29,5 +29,5 @@ public interface EmailAddressManager extends EntityManager<EmailAddress> {
void sendVerificationEmail(EmailAddress emailAddress);
EmailAddressFacades getCache();
EmailAddressFacades cloneCache();
}

View File

@ -294,8 +294,8 @@ public class DefaultEmailAddressManager extends BaseEntityManager<EmailAddress>
}
@Override
public EmailAddressFacades getCache() {
public EmailAddressFacades cloneCache() {
return cache.clone();
}
}

View File

@ -12,6 +12,7 @@ import javax.inject.Singleton;
import org.bouncycastle.openpgp.PGPPublicKey;
import io.onedev.commons.loader.Listen;
import io.onedev.server.entitymanager.EmailAddressManager;
import io.onedev.server.entitymanager.GpgKeyManager;
import io.onedev.server.event.entity.EntityPersisted;
import io.onedev.server.event.entity.EntityRemoved;
@ -32,12 +33,15 @@ public class DefaultGpgKeyManager extends BaseEntityManager<GpgKey> implements G
private final TransactionManager transactionManager;
private final EmailAddressManager emailAddressManager;
private final Map<Long, Long> entityIdCache = new ConcurrentHashMap<>();
@Inject
public DefaultGpgKeyManager(Dao dao, TransactionManager transactionManager) {
public DefaultGpgKeyManager(Dao dao, TransactionManager transactionManager, EmailAddressManager emailAddressManager) {
super(dao);
this.transactionManager = transactionManager;
this.emailAddressManager = emailAddressManager;
}
@Listen
@ -61,27 +65,12 @@ public class DefaultGpgKeyManager extends BaseEntityManager<GpgKey> implements G
entityIdCache.keySet().removeAll(keyIds);
}
});
} else if (event.getEntity() instanceof EmailAddress) {
EmailAddress emailAddress = (EmailAddress) event.getEntity();
Collection<Long> keyIds = new ArrayList<>();
for (GpgKey key: emailAddress.getGpgKeys())
keyIds.addAll(key.getKeyIds());
transactionManager.runAfterCommit(new Runnable() {
@Override
public void run() {
entityIdCache.keySet().removeAll(keyIds);
}
});
} else if (event.getEntity() instanceof User) {
User user = (User) event.getEntity();
Collection<Long> keyIds = new ArrayList<>();
for (EmailAddress emailAddress: user.getEmailAddresses()) {
for (GpgKey key: emailAddress.getGpgKeys())
keyIds.addAll(key.getKeyIds());
}
for (GpgKey key: user.getGpgKeys())
keyIds.addAll(key.getKeyIds());
transactionManager.runAfterCommit(new Runnable() {
@Override
@ -119,6 +108,8 @@ public class DefaultGpgKeyManager extends BaseEntityManager<GpgKey> implements G
if (entityId != null) {
return new SignatureVerificationKey() {
private transient List<String> emailAddresses;
@Override
public boolean shouldVerifyDataWriter() {
return true;
@ -134,8 +125,20 @@ public class DefaultGpgKeyManager extends BaseEntityManager<GpgKey> implements G
}
@Override
public String getEmailAddress() {
return GpgUtils.getEmailAddress(load(entityId).getPublicKeys().get(0));
public List<String> getEmailAddresses() {
if (emailAddresses == null) {
emailAddresses = new ArrayList<>();
GpgKey gpgKey = load(entityId);
for (String value: GpgUtils.getEmailAddresses(gpgKey.getPublicKeys().get(0))) {
EmailAddress emailAddress = emailAddressManager.findByValue(value);
if (emailAddress != null
&& emailAddress.isVerified()
&& emailAddress.getOwner().equals(gpgKey.getOwner())) {
emailAddresses.add(value);
}
}
}
return emailAddresses;
}
};

View File

@ -686,10 +686,10 @@ public class GitUtils {
key.getPublicKey());
signature.update(data);
if (signature.verify()) {
if (!key.shouldVerifyDataWriter() || key.getEmailAddress().equals(dataWriter))
if (!key.shouldVerifyDataWriter() || key.getEmailAddresses().contains(dataWriter))
return new SignatureVerified(key);
else
return new SignatureUnverified(key, "Email address of signing key and committer is different");
return new SignatureUnverified(key, "Can not verify committer email using signing key");
} else {
return new SignatureUnverified(key, "Invalid commit signature");
}

View File

@ -1,5 +1,7 @@
package io.onedev.server.git.signature;
import java.util.List;
import org.bouncycastle.openpgp.PGPPublicKey;
public interface SignatureVerificationKey {
@ -8,6 +10,6 @@ public interface SignatureVerificationKey {
PGPPublicKey getPublicKey();
String getEmailAddress();
List<String> getEmailAddresses();
}

View File

@ -4307,4 +4307,27 @@ public class DataMigrator {
}
}
private void migrate97(File dataDir, Stack<Integer> versions) {
Map<String, String> emailOwners = new HashMap<>();
for (File file: dataDir.listFiles()) {
if (file.getName().startsWith("EmailAddresss.xml")) {
VersionedXmlDoc dom = VersionedXmlDoc.fromFile(file);
for (Element element: dom.getRootElement().elements())
emailOwners.put(element.elementTextTrim("id"), element.elementTextTrim("owner"));
}
}
for (File file: dataDir.listFiles()) {
if (file.getName().startsWith("GpgKeys.xml")) {
VersionedXmlDoc dom = VersionedXmlDoc.fromFile(file);
for (Element element: dom.getRootElement().elements()) {
Element emailAddressElement = element.element("emailAddress");
element.addElement("owner").setText(emailOwners.get(emailAddressElement.getTextTrim()));
emailAddressElement.detach();
}
dom.writeToFile(file, false);
}
}
}
}

View File

@ -1,16 +1,11 @@
package io.onedev.server.model;
import java.util.ArrayList;
import java.util.Collection;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import org.apache.commons.lang3.RandomStringUtils;
@ -50,10 +45,6 @@ public class EmailAddress extends AbstractEntity {
@JoinColumn(nullable=false)
private User owner;
@OneToMany(mappedBy="emailAddress", cascade=CascadeType.REMOVE)
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
private Collection<GpgKey> gpgKeys = new ArrayList<>();
@Editable
@Email
@NotEmpty
@ -100,14 +91,6 @@ public class EmailAddress extends AbstractEntity {
this.owner = owner;
}
public Collection<GpgKey> getGpgKeys() {
return gpgKeys;
}
public void setGpgKeys(Collection<GpgKey> gpgKeys) {
this.gpgKeys = gpgKeys;
}
public boolean isVerified() {
return getVerificationCode() == null;
}

View File

@ -20,7 +20,7 @@ import io.onedev.server.web.editable.annotation.Editable;
@Editable
@Entity
@Table(indexes={@Index(columnList="o_emailAddress_id"), @Index(columnList="keyId")})
@Table(indexes={@Index(columnList="keyId")})
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
public class GpgKey extends BaseGpgKey {
@ -38,7 +38,7 @@ public class GpgKey extends BaseGpgKey {
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(nullable=false)
private EmailAddress emailAddress;
private User owner;
public long getKeyId() {
return keyId;
@ -48,12 +48,12 @@ public class GpgKey extends BaseGpgKey {
this.keyId = keyId;
}
public EmailAddress getEmailAddress() {
return emailAddress;
public User getOwner() {
return owner;
}
public void setEmailAddress(EmailAddress emailAddress) {
this.emailAddress = emailAddress;
public void setOwner(User owner) {
this.owner = owner;
}
public Date getCreatedAt() {

View File

@ -177,6 +177,10 @@ public class User extends AbstractEntity implements AuthenticationInfo {
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
private Collection<EmailAddress> emailAddresses = new ArrayList<>();
@OneToMany(mappedBy="owner", cascade=CascadeType.REMOVE)
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
private Collection<GpgKey> gpgKeys = new ArrayList<>();
@JsonIgnore
@Lob
@Column(nullable=false, length=65535)
@ -604,6 +608,14 @@ public class User extends AbstractEntity implements AuthenticationInfo {
this.emailAddresses = emailAddresses;
}
public Collection<GpgKey> getGpgKeys() {
return gpgKeys;
}
public void setGpgKeys(Collection<GpgKey> gpgKeys) {
this.gpgKeys = gpgKeys;
}
public boolean isSshKeyExternalManaged() {
if (isExternalManaged()) {
if (getSsoConnector() != null) {
@ -812,14 +824,6 @@ public class User extends AbstractEntity implements AuthenticationInfo {
return gitEmailAddress.orElse(null);
}
public List<GpgKey> getGpgKeys() {
List<GpgKey> gpgKeys = new ArrayList<>();
for (EmailAddress emailAddress: getEmailAddresses())
gpgKeys.addAll(emailAddress.getGpgKeys());
Collections.sort(gpgKeys);
return gpgKeys;
}
public UserFacade getFacade() {
return new UserFacade(getId(), getName(), getFullName(), getAccessToken());
}

View File

@ -115,8 +115,8 @@ public class GpgSetting implements Serializable {
}
@Override
public String getEmailAddress() {
return GpgUtils.getEmailAddress(publicKeys.get(0));
public List<String> getEmailAddresses() {
return GpgUtils.getEmailAddresses(publicKeys.get(0));
}
});
@ -142,8 +142,8 @@ public class GpgSetting implements Serializable {
}
@Override
public String getEmailAddress() {
return GpgUtils.getEmailAddress(getPublicKey());
public List<String> getEmailAddresses() {
return GpgUtils.getEmailAddresses(getPublicKey());
}
};

View File

@ -58,12 +58,12 @@ public class GpgUtils {
}
}
public static String getEmailAddress(PGPPublicKey publicKey) {
public static List<String> getEmailAddresses(PGPPublicKey publicKey) {
List<String> emailAddresses = new ArrayList<>();
Iterator<String> it = publicKey.getUserIDs();
if (it.hasNext())
return getEmailAddress(it.next());
else
throw new ExplicitException("No email found");
while (it.hasNext())
emailAddresses.add(getEmailAddress(it.next()));
return emailAddresses;
}
public static String getEmailAddress(String userId) {

View File

@ -8,8 +8,8 @@
<div wicket:id="keyId" class="text-nowrap"></div>
</div>
<div>
<div class="font-weight-bolder mb-2 text-nowrap">Signer Email Address</div>
<div wicket:id="emailAddress" class="text-nowrap"></div>
<div class="font-weight-bolder mb-2 text-nowrap">Signer Email Addresses</div>
<div wicket:id="emailAddresses" class="text-nowrap"></div>
</div>
</div>
</wicket:enclosure>

View File

@ -7,6 +7,7 @@ import org.apache.wicket.markup.html.panel.Panel;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import io.onedev.commons.utils.StringUtils;
import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.SettingManager;
import io.onedev.server.git.signature.SignatureUnverified;
@ -15,6 +16,7 @@ import io.onedev.server.git.signature.SignatureVerificationKey;
import io.onedev.server.git.signature.SignatureVerified;
import io.onedev.server.model.support.administration.GpgSetting;
import io.onedev.server.util.GpgUtils;
import io.onedev.server.web.component.MultilineLabel;
@SuppressWarnings("serial")
abstract class GitSignatureDetailPanel extends Panel {
@ -54,7 +56,7 @@ abstract class GitSignatureDetailPanel extends Panel {
if (key != null) {
add(new Label("keyId", GpgUtils.getKeyIDString(key.getPublicKey().getKeyID())));
add(new Label("emailAddress", key.getEmailAddress()));
add(new MultilineLabel("emailAddresses", StringUtils.join(key.getEmailAddresses(), "\n")));
} else {
add(new WebMarkupContainer("keyId").setVisible(false));
add(new WebMarkupContainer("emailAddress").setVisible(false));

View File

@ -1,6 +1,14 @@
<wicket:panel>
<table wicket:id="keys" class="gpg-key-list table table-hover"></table>
<div class="text-muted font-size-sm mt-4"><wicket:svg href="bulb" class="icon"/> Check <a href="https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification#gpg-commit-signature-verification" target="_blank">GitHub's guide</a> on how to generate and use GPG keys to sign your commits</div>
<ul class="text-muted font-size-sm mt-4 ml-n4">
<li>Check <a href="https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification#gpg-commit-signature-verification" target="_blank">GitHub's guide</a> on how to generate and use GPG keys to sign your commits</li>
<li>Email addresses with <span class="badge badge-warning badge-sm">effective</span> mark are those not belong to or not verified by key owner</li>
</ul>
<wicket:fragment wicket:id="emailAddressesFrag">
<div wicket:id="values" class="value text-nowrap">
<span wicket:id="value"></span> <span wicket:id="ineffective" class="badge badge-sm badge-warning">ineffective</span>
</div>
</wicket:fragment>
<wicket:fragment wicket:id="actionFrag">
<a wicket:id="delete" class="btn btn-xs btn-icon btn-light btn-hover-danger" title="Delete this key"><wicket:svg href="trash" class="icon"></wicket:svg></a>
</wicket:fragment>

View File

@ -16,19 +16,24 @@ import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider;
import org.apache.wicket.markup.head.CssHeaderItem;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.markup.html.panel.GenericPanel;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.RepeatingView;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.EmailAddressManager;
import io.onedev.server.entitymanager.GpgKeyManager;
import io.onedev.server.model.EmailAddress;
import io.onedev.server.model.GpgKey;
import io.onedev.server.util.GpgUtils;
import io.onedev.server.web.ajaxlistener.ConfirmClickListener;
import io.onedev.server.web.component.MultilineLabel;
import io.onedev.server.web.component.datatable.DefaultDataTable;
import io.onedev.server.web.util.LoadableDetachableDataProvider;
@ -51,16 +56,6 @@ public class GpgKeyListPanel extends GenericPanel<List<GpgKey>> {
List<IColumn<GpgKey, Void>> columns = new ArrayList<>();
columns.add(new AbstractColumn<GpgKey, Void>(Model.of("Email Address")) {
@Override
public void populateItem(Item<ICellPopulator<GpgKey>> cellItem, String componentId,
IModel<GpgKey> rowModel) {
cellItem.add(new Label(componentId, rowModel.getObject().getEmailAddress().getValue()));
}
});
columns.add(new AbstractColumn<GpgKey, Void>(Model.of("Key ID")) {
@Override
@ -72,6 +67,37 @@ public class GpgKeyListPanel extends GenericPanel<List<GpgKey>> {
});
columns.add(new AbstractColumn<GpgKey, Void>(Model.of("Email Addresses")) {
@Override
public void populateItem(Item<ICellPopulator<GpgKey>> cellItem, String componentId,
IModel<GpgKey> rowModel) {
GpgKey key = rowModel.getObject();
Fragment fragment = new Fragment(componentId, "emailAddressesFrag", GpgKeyListPanel.this);
RepeatingView valuesView = new RepeatingView("values");
EmailAddressManager emailAddressManager = OneDev.getInstance(EmailAddressManager.class);
for (String emailAddressValue: GpgUtils.getEmailAddresses(key.getPublicKeys().get(0))) {
WebMarkupContainer container = new WebMarkupContainer(valuesView.newChildId());
valuesView.add(container);
container.add(new Label("value", emailAddressValue));
EmailAddress emailAddress = emailAddressManager.findByValue(emailAddressValue);
boolean unverified = emailAddress == null
|| !emailAddress.isVerified()
|| !emailAddress.getOwner().equals(key.getOwner());
container.add(new WebMarkupContainer("ineffective").setVisible(unverified));
}
fragment.add(valuesView);
cellItem.add(fragment);
}
@Override
public String getCssClass() {
return "email-addresses";
}
});
columns.add(new AbstractColumn<GpgKey, Void>(Model.of("Sub Keys")) {
@Override
@ -86,9 +112,9 @@ public class GpgKeyListPanel extends GenericPanel<List<GpgKey>> {
String subKeyIds = key.getKeyIds().stream()
.filter(it->it!=key.getKeyId())
.map(it->GpgUtils.getKeyIDString(it))
.collect(Collectors.joining(", "));
.collect(Collectors.joining("\n"));
if (subKeyIds.length() != 0)
cellItem.add(new Label(componentId, subKeyIds));
cellItem.add(new MultilineLabel(componentId, subKeyIds));
else
cellItem.add(new Label(componentId, "<i>None</i>").setEscapeModelStrings(false));
}

View File

@ -51,29 +51,34 @@ public abstract class InsertGpgKeyPanel extends Panel {
form.add(new AjaxButton("add") {
@Override
protected void onSubmit(AjaxRequestTarget target, Form<?> myform) {
super.onSubmit(target, myform);
protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
super.onSubmit(target, form);
GpgKeyManager gpgKeyManager = OneDev.getInstance(GpgKeyManager.class);
GpgKey gpgKey = (GpgKey) editor.getModelObject();
gpgKey.setOwner(getUser());
gpgKey.setKeyId(gpgKey.getKeyIds().get(0));
if (gpgKey.getKeyIds().stream().anyMatch(it->gpgKeyManager.findSignatureVerificationKey(it)!=null)) {
editor.error(new Path(new PathNode.Named("content")), "This key or one of its subkey is already in use");
target.add(form);
} else {
String emailAddressValue = GpgUtils.getEmailAddress(gpgKey.getPublicKeys().get(0));
EmailAddress emailAddress = OneDev.getInstance(EmailAddressManager.class).findByValue(emailAddressValue);
if (emailAddress != null && emailAddress.isVerified() && emailAddress.getOwner().equals(getUser())) {
gpgKey.setEmailAddress(emailAddress);
boolean hasErrors = false;
for (String emailAddressValue: GpgUtils.getEmailAddresses(gpgKey.getPublicKeys().get(0))) {
EmailAddress emailAddress = OneDev.getInstance(EmailAddressManager.class).findByValue(emailAddressValue);
if (emailAddress == null || !emailAddress.isVerified() || !emailAddress.getOwner().equals(getUser())) {
String who = (getPage() instanceof MyPage)? "yours": "the user";
editor.error(new Path(new PathNode.Named("content")), "This key is associated with " + emailAddressValue
+ ", however it is NOT a verified email address of " + who);
target.add(form);
hasErrors = true;
break;
}
}
if (!hasErrors) {
gpgKey.setCreatedAt(new Date());
gpgKeyManager.save(gpgKey);
onSave(target);
} else {
String who = (getPage() instanceof MyPage)? "yours": "the user";
editor.error(new Path(new PathNode.Named("content")), "This key is associated with " + emailAddressValue
+ ", however it is NOT a verified email address of " + who);
target.add(form);
}
}
}

View File

@ -4,6 +4,12 @@
}
}
.gpg-key-list .email-addresses .value {
margin-right: 0.5rem;
}
.gpg-key-list .email-addresses .value:last-child {
margin-right: 0;
}
.insert-gpg-key textarea {
font-family: monospace;
height: 200px !important;

View File

@ -15,6 +15,7 @@ import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPKeyRingGenerator;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import io.onedev.commons.utils.StringUtils;
import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.SettingManager;
import io.onedev.server.model.User;
@ -40,7 +41,7 @@ public class GpgSigningKeyPage extends AdministrationPage {
if (signingKey != null) {
Fragment fragment = new Fragment("content", "definedFrag", this);
fragment.add(new Label("keyID", GpgUtils.getKeyIDString(signingKey.getPublicKey().getKeyID())));
fragment.add(new Label("emailAddress", GpgUtils.getEmailAddress(signingKey.getPublicKey())));
fragment.add(new Label("emailAddress", StringUtils.join(GpgUtils.getEmailAddresses(signingKey.getPublicKey()), ", ")));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ArmoredOutputStream aos = new ArmoredOutputStream(baos)) {

View File

@ -27,6 +27,7 @@ import org.apache.wicket.model.Model;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.bouncycastle.openpgp.PGPPublicKey;
import io.onedev.commons.utils.StringUtils;
import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.SettingManager;
import io.onedev.server.model.support.BaseGpgKey;
@ -35,6 +36,7 @@ import io.onedev.server.util.GpgUtils;
import io.onedev.server.util.Path;
import io.onedev.server.util.PathNode;
import io.onedev.server.web.ajaxlistener.ConfirmClickListener;
import io.onedev.server.web.component.MultilineLabel;
import io.onedev.server.web.component.datatable.DefaultDataTable;
import io.onedev.server.web.component.modal.ModalLink;
import io.onedev.server.web.component.modal.ModalPanel;
@ -130,17 +132,6 @@ public class GpgTrustedKeysPage extends AdministrationPage {
List<IColumn<Long, Void>> columns = new ArrayList<>();
columns.add(new AbstractColumn<Long, Void>(Model.of("Email Address")) {
@Override
public void populateItem(Item<ICellPopulator<Long>> cellItem, String componentId,
IModel<Long> rowModel) {
List<PGPPublicKey> trustedKey = getTrustedKey(rowModel.getObject());
cellItem.add(new Label(componentId, GpgUtils.getEmailAddress(trustedKey.get(0))));
}
});
columns.add(new AbstractColumn<Long, Void>(Model.of("Key ID")) {
@Override
@ -152,6 +143,18 @@ public class GpgTrustedKeysPage extends AdministrationPage {
});
columns.add(new AbstractColumn<Long, Void>(Model.of("Email Addresses")) {
@Override
public void populateItem(Item<ICellPopulator<Long>> cellItem, String componentId,
IModel<Long> rowModel) {
List<PGPPublicKey> trustedKey = getTrustedKey(rowModel.getObject());
String joined = StringUtils.join(GpgUtils.getEmailAddresses(trustedKey.get(0)), "\n");
cellItem.add(new MultilineLabel(componentId, joined));
}
});
columns.add(new AbstractColumn<Long, Void>(Model.of("Sub Keys")) {
@Override
@ -163,9 +166,9 @@ public class GpgTrustedKeysPage extends AdministrationPage {
.map(it->it.getKeyID())
.filter(it->it!=keyId)
.map(it->GpgUtils.getKeyIDString(it))
.collect(Collectors.joining(", "));
.collect(Collectors.joining("\n"));
if (subKeyIds.length() != 0)
cellItem.add(new Label(componentId, subKeyIds));
cellItem.add(new MultilineLabel(componentId, subKeyIds));
else
cellItem.add(new Label(componentId, "<i>None</i>").setEscapeModelStrings(false));
}

View File

@ -1,5 +1,6 @@
package io.onedev.server.web.page.admin.user.gpgkeys;
import java.util.ArrayList;
import java.util.List;
import org.apache.wicket.Component;
@ -8,6 +9,7 @@ import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import edu.emory.mathcs.backport.java.util.Collections;
import io.onedev.server.model.GpgKey;
import io.onedev.server.model.User;
import io.onedev.server.web.component.modal.ModalLink;
@ -65,7 +67,9 @@ public class UserGpgKeysPage extends UserPage {
@Override
protected List<GpgKey> load() {
return getUser().getGpgKeys();
List<GpgKey> gpgKeys = new ArrayList<>(getUser().getGpgKeys());
Collections.sort(gpgKeys);
return gpgKeys;
}
});

View File

@ -1,5 +1,6 @@
package io.onedev.server.web.page.my.gpgkeys;
import java.util.ArrayList;
import java.util.List;
import org.apache.wicket.Component;
@ -8,6 +9,7 @@ import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import edu.emory.mathcs.backport.java.util.Collections;
import io.onedev.server.model.GpgKey;
import io.onedev.server.model.User;
import io.onedev.server.web.component.modal.ModalLink;
@ -65,7 +67,9 @@ public class MyGpgKeysPage extends MyPage {
@Override
protected List<GpgKey> load() {
return getLoginUser().getGpgKeys();
List<GpgKey> gpgKeys = new ArrayList<>(getLoginUser().getGpgKeys());
Collections.sort(gpgKeys);
return gpgKeys;
}
});