From 4210dc0c6d956ff1bad372479f2c24a97e155898 Mon Sep 17 00:00:00 2001
From: Sebastian Sdorra
Date: Mon, 20 Jul 2020 15:07:11 +0200
Subject: [PATCH 01/46] implemented public api for verifying changeset
signatures
---
.../java/sonia/scm/repository/Changeset.java | 33 +++++++++++++
.../java/sonia/scm/repository/Signature.java | 47 +++++++++++++++++++
2 files changed, 80 insertions(+)
create mode 100644 scm-core/src/main/java/sonia/scm/repository/Signature.java
diff --git a/scm-core/src/main/java/sonia/scm/repository/Changeset.java b/scm-core/src/main/java/sonia/scm/repository/Changeset.java
index e2c980db37..5edd01531e 100644
--- a/scm-core/src/main/java/sonia/scm/repository/Changeset.java
+++ b/scm-core/src/main/java/sonia/scm/repository/Changeset.java
@@ -32,6 +32,7 @@ import sonia.scm.util.ValidationUtil;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.Date;
import java.util.List;
@@ -85,6 +86,8 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
*/
private Collection contributors;
+ private List signatures;
+
public Changeset() {}
public Changeset(String id, Long date, Person author)
@@ -348,4 +351,34 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
this.contributors.addAll(contributors);
}
}
+
+ /**
+ * Sets a collection of signatures which belong to this changeset.
+ * @param signatures collection of signatures
+ * @since 2.3.0
+ */
+ public void setSignatures(Collection signatures) {
+ this.signatures = new ArrayList<>(signatures);
+ }
+
+ /**
+ * Returns a immutable list of signatures.
+ * @return signatures
+ * @since 2.3.0
+ */
+ public List getSignatures() {
+ return Collections.unmodifiableList(signatures);
+ }
+
+ /**
+ * Adds a signature to the list of signatures.
+ * @param signature
+ * @since 2.3.0
+ */
+ public void addSignature(Signature signature) {
+ if (signatures == null) {
+ signatures = new ArrayList<>();
+ }
+ signatures.add(signature);
+ }
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/Signature.java b/scm-core/src/main/java/sonia/scm/repository/Signature.java
new file mode 100644
index 0000000000..4cada0e629
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/Signature.java
@@ -0,0 +1,47 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.repository;
+
+import lombok.Value;
+
+import java.util.Optional;
+
+/**
+ * Signature is the output of a signature verification.
+ * @since 2.3.0
+ */
+@Value
+public class Signature {
+
+ private final String key;
+ private final String type;
+ private final boolean verified;
+ private final String owner;
+
+ public Optional getOwner() {
+ return Optional.ofNullable(owner);
+ }
+
+}
From 1015445ba57300b3981eae235c7d4071ed202949 Mon Sep 17 00:00:00 2001
From: Sebastian Sdorra
Date: Mon, 20 Jul 2020 15:07:35 +0200
Subject: [PATCH 02/46] added public api for resolving public gpg keys
---
.../java/sonia/scm/security/GPGPublicKey.java | 40 ++++++++++++++++
.../scm/security/GPGPublicKeyResolver.java | 48 +++++++++++++++++++
2 files changed, 88 insertions(+)
create mode 100644 scm-core/src/main/java/sonia/scm/security/GPGPublicKey.java
create mode 100644 scm-core/src/main/java/sonia/scm/security/GPGPublicKeyResolver.java
diff --git a/scm-core/src/main/java/sonia/scm/security/GPGPublicKey.java b/scm-core/src/main/java/sonia/scm/security/GPGPublicKey.java
new file mode 100644
index 0000000000..731b958c09
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/security/GPGPublicKey.java
@@ -0,0 +1,40 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security;
+
+import lombok.Value;
+
+/**
+ * Public gpg key.
+ * @since 2.3.0
+ */
+@Value
+public class GPGPublicKey {
+
+ private final String id;
+ private final String owner;
+ private final String raw;
+
+}
diff --git a/scm-core/src/main/java/sonia/scm/security/GPGPublicKeyResolver.java b/scm-core/src/main/java/sonia/scm/security/GPGPublicKeyResolver.java
new file mode 100644
index 0000000000..56158ec36a
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/security/GPGPublicKeyResolver.java
@@ -0,0 +1,48 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Resolver for public gpg keys.
+ * @since 2.3.0
+ */
+public interface GPGPublicKeyResolver {
+
+ /**
+ * Resolves the public key by its id.
+ * @param keyId id of the key
+ * @return public gpg key or empty optional.
+ */
+ Optional byId(String keyId);
+
+ /**
+ * Resolves all public gpg keys for the given user.
+ * @return list of public gpg keys
+ */
+ List byUser(String username);
+}
From 6e27051ed9987b087d71238979162c06dc642314 Mon Sep 17 00:00:00 2001
From: Sebastian Sdorra
Date: Mon, 20 Jul 2020 16:17:18 +0200
Subject: [PATCH 03/46] use child injector pattern for git commands
We are using a Google Guice child injector to reduce the amount of injection dependencies for the GitRepositoryServiceResolver and the GitRepositoryServiceProvider.
---
scm-core/pom.xml | 6 +
.../scm/repository/spi/GitBlameCommand.java | 2 +
.../scm/repository/spi/GitBranchCommand.java | 2 +
.../repository/spi/GitBranchesCommand.java | 2 +
.../scm/repository/spi/GitBrowseCommand.java | 6 +
.../scm/repository/spi/GitCatCommand.java | 2 +
.../scm/repository/spi/GitContextFactory.java | 48 ++++
.../scm/repository/spi/GitDiffCommand.java | 2 +
.../repository/spi/GitDiffResultCommand.java | 2 +
.../repository/spi/GitIncomingCommand.java | 2 +
.../scm/repository/spi/GitLogCommand.java | 2 +
.../scm/repository/spi/GitMergeCommand.java | 7 +
.../spi/GitModificationsCommand.java | 4 +-
.../scm/repository/spi/GitModifyCommand.java | 7 +
.../repository/spi/GitOutgoingCommand.java | 2 +
.../scm/repository/spi/GitPullCommand.java | 2 +
.../scm/repository/spi/GitPushCommand.java | 5 +-
.../spi/GitRepositoryServiceProvider.java | 210 ++++--------------
.../spi/GitRepositoryServiceResolver.java | 32 +--
.../repository/spi/GitDiffCommandTest.java | 1 -
.../spi/GitRepositoryServiceProviderTest.java | 74 ++++++
.../spi/GitRepositoryServiceResolverTest.java | 63 ++++++
scm-webapp/pom.xml | 6 -
23 files changed, 290 insertions(+), 199 deletions(-)
create mode 100644 scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContextFactory.java
create mode 100644 scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryServiceProviderTest.java
create mode 100644 scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryServiceResolverTest.java
diff --git a/scm-core/pom.xml b/scm-core/pom.xml
index 2ada8e2e76..1cadd49ae7 100644
--- a/scm-core/pom.xml
+++ b/scm-core/pom.xml
@@ -112,6 +112,12 @@
${guice.version}
+
+ com.google.inject.extensions
+ guice-assistedinject
+ ${guice.version}
+
+
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java
index 5c1f074a90..d99343b350 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java
@@ -41,6 +41,7 @@ import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Person;
+import javax.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -64,6 +65,7 @@ public class GitBlameCommand extends AbstractGitCommand implements BlameCommand
//~--- constructors ---------------------------------------------------------
+ @Inject
public GitBlameCommand(GitContext context)
{
super(context);
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java
index 506580d5a9..82a62ef111 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java
@@ -42,6 +42,7 @@ import sonia.scm.repository.api.HookContext;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.repository.api.HookFeature;
+import javax.inject.Inject;
import java.io.IOException;
import java.util.List;
import java.util.Set;
@@ -56,6 +57,7 @@ public class GitBranchCommand extends AbstractGitCommand implements BranchComman
private final HookContextFactory hookContextFactory;
private final ScmEventBus eventBus;
+ @Inject
GitBranchCommand(GitContext context, HookContextFactory hookContextFactory, ScmEventBus eventBus) {
super(context);
this.hookContextFactory = hookContextFactory;
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java
index cd7892461b..d026affd8b 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java
@@ -38,6 +38,7 @@ import sonia.scm.repository.Branch;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
+import javax.inject.Inject;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
@@ -53,6 +54,7 @@ public class GitBranchesCommand extends AbstractGitCommand implements BranchesCo
private static final Logger LOG = LoggerFactory.getLogger(GitBranchesCommand.class);
+ @Inject
public GitBranchesCommand(GitContext context)
{
super(context);
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java
index ab5a7d33b4..05792b9707 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java
@@ -57,6 +57,7 @@ import sonia.scm.store.BlobStore;
import sonia.scm.util.Util;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
+import javax.inject.Inject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
@@ -111,6 +112,11 @@ public class GitBrowseCommand extends AbstractGitCommand
private int resultCount = 0;
+ @Inject
+ public GitBrowseCommand(GitContext context, LfsBlobStoreFactory lfsBlobStoreFactory, SyncAsyncExecutorProvider executorProvider) {
+ this(context, lfsBlobStoreFactory, executorProvider.createExecutorWithDefaultTimeout());
+ }
+
public GitBrowseCommand(GitContext context, LfsBlobStoreFactory lfsBlobStoreFactory, SyncAsyncExecutor executor) {
super(context);
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java
index 4629e4bce2..8c2e3b44d5 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java
@@ -44,6 +44,7 @@ import sonia.scm.util.IOUtil;
import sonia.scm.util.Util;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
+import javax.inject.Inject;
import java.io.Closeable;
import java.io.FilterInputStream;
import java.io.IOException;
@@ -61,6 +62,7 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
private final LfsBlobStoreFactory lfsBlobStoreFactory;
+ @Inject
public GitCatCommand(GitContext context, LfsBlobStoreFactory lfsBlobStoreFactory) {
super(context);
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContextFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContextFactory.java
new file mode 100644
index 0000000000..04bb37bed0
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContextFactory.java
@@ -0,0 +1,48 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.repository.spi;
+
+import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
+import sonia.scm.repository.GitRepositoryHandler;
+import sonia.scm.repository.Repository;
+
+import javax.inject.Inject;
+
+class GitContextFactory {
+
+ private final GitRepositoryHandler handler;
+ private final GitRepositoryConfigStoreProvider storeProvider;
+
+ @Inject
+ GitContextFactory(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider) {
+ this.handler = handler;
+ this.storeProvider = storeProvider;
+ }
+
+ GitContext create(Repository repository) {
+ return new GitContext(handler.getDirectory(repository.getId()), repository, storeProvider);
+ }
+
+}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java
index adb7a7bd0e..c0ed1a53bc 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java
@@ -29,6 +29,7 @@ import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.util.QuotedString;
import sonia.scm.repository.api.DiffCommandBuilder;
+import javax.inject.Inject;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -41,6 +42,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
*/
public class GitDiffCommand extends AbstractGitCommand implements DiffCommand {
+ @Inject
GitDiffCommand(GitContext context) {
super(context);
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java
index fed865c576..e55d8badae 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java
@@ -32,6 +32,7 @@ import sonia.scm.repository.api.DiffFile;
import sonia.scm.repository.api.DiffResult;
import sonia.scm.repository.api.Hunk;
+import javax.inject.Inject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Iterator;
@@ -39,6 +40,7 @@ import java.util.stream.Collectors;
public class GitDiffResultCommand extends AbstractGitCommand implements DiffResultCommand {
+ @Inject
GitDiffResultCommand(GitContext context) {
super(context);
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitIncomingCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitIncomingCommand.java
index f6e818bcdb..638983b6f1 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitIncomingCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitIncomingCommand.java
@@ -31,6 +31,7 @@ import org.eclipse.jgit.lib.ObjectId;
import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitRepositoryHandler;
+import javax.inject.Inject;
import java.io.IOException;
//~--- JDK imports ------------------------------------------------------------
@@ -49,6 +50,7 @@ public class GitIncomingCommand extends AbstractGitIncomingOutgoingCommand
* @param handler
* @param context
*/
+ @Inject
GitIncomingCommand(GitRepositoryHandler handler, GitContext context)
{
super(handler, context);
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java
index da00ba451b..b1205de975 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java
@@ -47,6 +47,7 @@ import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.util.IOUtil;
+import javax.inject.Inject;
import java.io.IOException;
import java.util.Collections;
import java.util.Iterator;
@@ -80,6 +81,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
* @param context
*
*/
+ @Inject
GitLogCommand(GitContext context)
{
super(context);
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java
index 9f362edeb6..bd4e6b26b5 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java
@@ -36,6 +36,7 @@ import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.merge.ResolveMerger;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.filter.PathFilter;
+import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.GitWorkingCopyFactory;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.api.MergeCommandResult;
@@ -43,6 +44,7 @@ import sonia.scm.repository.api.MergeDryRunCommandResult;
import sonia.scm.repository.api.MergeStrategy;
import sonia.scm.repository.api.MergeStrategyNotSupportedException;
+import javax.inject.Inject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Set;
@@ -61,6 +63,11 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
MergeStrategy.SQUASH
);
+ @Inject
+ GitMergeCommand(GitContext context, GitRepositoryHandler handler) {
+ this(context, handler.getWorkingCopyFactory());
+ }
+
GitMergeCommand(GitContext context, GitWorkingCopyFactory workingCopyFactory) {
super(context);
this.workingCopyFactory = workingCopyFactory;
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java
index 918d276148..e907081f2c 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java
@@ -41,6 +41,7 @@ import sonia.scm.repository.Modified;
import sonia.scm.repository.Removed;
import sonia.scm.repository.Renamed;
+import javax.inject.Inject;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
@@ -53,7 +54,8 @@ import static sonia.scm.ContextEntry.ContextBuilder.entity;
@Slf4j
public class GitModificationsCommand extends AbstractGitCommand implements ModificationsCommand {
- protected GitModificationsCommand(GitContext context) {
+ @Inject
+ GitModificationsCommand(GitContext context) {
super(context);
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java
index 9e0f5449d3..7f32f55e52 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java
@@ -34,11 +34,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ConcurrentModificationException;
import sonia.scm.NoChangesMadeException;
+import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.GitWorkingCopyFactory;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
+import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
@@ -53,6 +55,11 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman
private final GitWorkingCopyFactory workingCopyFactory;
private final LfsBlobStoreFactory lfsBlobStoreFactory;
+ @Inject
+ GitModifyCommand(GitContext context, GitRepositoryHandler repositoryHandler, LfsBlobStoreFactory lfsBlobStoreFactory) {
+ this(context, repositoryHandler.getWorkingCopyFactory(), lfsBlobStoreFactory);
+ }
+
GitModifyCommand(GitContext context, GitWorkingCopyFactory workingCopyFactory, LfsBlobStoreFactory lfsBlobStoreFactory) {
super(context);
this.workingCopyFactory = workingCopyFactory;
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java
index 03acf9e914..bfb319df10 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java
@@ -31,6 +31,7 @@ import org.eclipse.jgit.lib.ObjectId;
import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitRepositoryHandler;
+import javax.inject.Inject;
import java.io.IOException;
//~--- JDK imports ------------------------------------------------------------
@@ -49,6 +50,7 @@ public class GitOutgoingCommand extends AbstractGitIncomingOutgoingCommand
* @param handler
* @param context
*/
+ @Inject
GitOutgoingCommand(GitRepositoryHandler handler, GitContext context)
{
super(handler, context);
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java
index 53b7a59916..422391fd19 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java
@@ -44,6 +44,7 @@ import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.PullResponse;
+import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.net.URL;
@@ -73,6 +74,7 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand
* @param handler
* @param context
*/
+ @Inject
public GitPullCommand(GitRepositoryHandler handler, GitContext context)
{
super(handler, context);
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPushCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPushCommand.java
index fd874524f4..ddbfb6a8e1 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPushCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPushCommand.java
@@ -31,6 +31,7 @@ import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.api.PushResponse;
+import javax.inject.Inject;
import java.io.IOException;
//~--- JDK imports ------------------------------------------------------------
@@ -55,8 +56,8 @@ public class GitPushCommand extends AbstractGitPushOrPullCommand
* @param handler
* @param context
*/
- public GitPushCommand(GitRepositoryHandler handler, GitContext context)
- {
+ @Inject
+ public GitPushCommand(GitRepositoryHandler handler, GitContext context) {
super(handler, context);
this.handler = handler;
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java
index 862631c32c..75b0f97b3f 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java
@@ -25,21 +25,14 @@
package sonia.scm.repository.spi;
import com.google.common.collect.ImmutableSet;
-import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
-import sonia.scm.event.ScmEventBus;
+import com.google.inject.AbstractModule;
+import com.google.inject.Injector;
import sonia.scm.repository.Feature;
-import sonia.scm.repository.GitRepositoryHandler;
-import sonia.scm.repository.Repository;
import sonia.scm.repository.api.Command;
-import sonia.scm.repository.api.HookContextFactory;
-import sonia.scm.web.lfs.LfsBlobStoreFactory;
-import java.io.IOException;
import java.util.EnumSet;
import java.util.Set;
-//~--- JDK imports ------------------------------------------------------------
-
/**
*
* @author Sebastian Sdorra
@@ -47,8 +40,6 @@ import java.util.Set;
public class GitRepositoryServiceProvider extends RepositoryServiceProvider
{
- /** Field description */
- //J-
public static final Set COMMANDS = ImmutableSet.of(
Command.BLAME,
Command.BROWSE,
@@ -66,105 +57,51 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
Command.MERGE,
Command.MODIFY
);
+
protected static final Set FEATURES = EnumSet.of(Feature.INCOMING_REVISION);
- //J+
+
+ private final GitContext context;
+ private final Injector commandInjector;
//~--- constructors ---------------------------------------------------------
- public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus, SyncAsyncExecutorProvider executorProvider) {
- this.handler = handler;
- this.lfsBlobStoreFactory = lfsBlobStoreFactory;
- this.hookContextFactory = hookContextFactory;
- this.eventBus = eventBus;
- this.executorProvider = executorProvider;
- this.context = new GitContext(handler.getDirectory(repository.getId()), repository, storeProvider);
+ GitRepositoryServiceProvider(Injector injector, GitContext context) {
+ this.context = context;
+ commandInjector = injector.createChildInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(GitContext.class).toInstance(context);
+ }
+ });
}
- //~--- methods --------------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @throws IOException
- */
@Override
- public void close() throws IOException
- {
- context.close();
- }
-
- //~--- get methods ----------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @return
- */
- @Override
- public BlameCommand getBlameCommand()
- {
+ public BlameCommand getBlameCommand() {
return new GitBlameCommand(context);
}
- /**
- * Method description
- *
- *
- * @return
- */
@Override
- public BranchesCommand getBranchesCommand()
- {
+ public BranchesCommand getBranchesCommand() {
return new GitBranchesCommand(context);
}
- /**
- * Method description
- *
- *
- * @return
- */
@Override
- public BranchCommand getBranchCommand()
- {
- return new GitBranchCommand(context, hookContextFactory, eventBus);
+ public BranchCommand getBranchCommand() {
+ return commandInjector.getInstance(GitBranchCommand.class);
}
- /**
- * Method description
- *
- *
- * @return
- */
@Override
- public BrowseCommand getBrowseCommand()
- {
- return new GitBrowseCommand(context, lfsBlobStoreFactory, executorProvider.createExecutorWithDefaultTimeout());
+ public BrowseCommand getBrowseCommand() {
+ return commandInjector.getInstance(GitBrowseCommand.class);
}
- /**
- * Method description
- *
- *
- * @return
- */
@Override
- public CatCommand getCatCommand()
- {
- return new GitCatCommand(context, lfsBlobStoreFactory);
+ public CatCommand getCatCommand() {
+ return commandInjector.getInstance(GitCatCommand.class);
}
- /**
- * Method description
- *
- *
- * @return
- */
@Override
- public DiffCommand getDiffCommand()
- {
+ public DiffCommand getDiffCommand() {
return new GitDiffCommand(context);
}
@@ -173,27 +110,13 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
return new GitDiffResultCommand(context);
}
- /**
- * Method description
- *
- *
- * @return
- */
@Override
- public IncomingCommand getIncomingCommand()
- {
- return new GitIncomingCommand(handler, context);
+ public IncomingCommand getIncomingCommand() {
+ return commandInjector.getInstance(GitIncomingCommand.class);
}
- /**
- * Method description
- *
- *
- * @return
- */
@Override
- public LogCommand getLogCommand()
- {
+ public LogCommand getLogCommand() {
return new GitLogCommand(context);
}
@@ -202,93 +125,48 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
return new GitModificationsCommand(context);
}
- /**
- * Method description
- *
- *
- * @return
- */
@Override
- public OutgoingCommand getOutgoingCommand()
- {
- return new GitOutgoingCommand(handler, context);
+ public OutgoingCommand getOutgoingCommand() {
+ return commandInjector.getInstance(GitOutgoingCommand.class);
}
- /**
- * Method description
- *
- *
- * @return
- */
@Override
- public PullCommand getPullCommand()
- {
- return new GitPullCommand(handler, context);
+ public PullCommand getPullCommand() {
+ return commandInjector.getInstance(GitPullCommand.class);
}
- /**
- * Method description
- *
- *
- * @return
- */
@Override
- public PushCommand getPushCommand()
- {
- return new GitPushCommand(handler, context);
+ public PushCommand getPushCommand() {
+ return commandInjector.getInstance(GitPushCommand.class);
}
- /**
- * Method description
- *
- *
- * @return
- */
@Override
- public Set getSupportedCommands()
- {
- return COMMANDS;
- }
-
- /**
- * Method description
- *
- *
- * @return
- */
- @Override
- public TagsCommand getTagsCommand()
- {
+ public TagsCommand getTagsCommand() {
return new GitTagsCommand(context);
}
@Override
public MergeCommand getMergeCommand() {
- return new GitMergeCommand(context, handler.getWorkingCopyFactory());
+ return commandInjector.getInstance(GitMergeCommand.class);
}
@Override
public ModifyCommand getModifyCommand() {
- return new GitModifyCommand(context, handler.getWorkingCopyFactory(), lfsBlobStoreFactory);
+ return commandInjector.getInstance(GitModifyCommand.class);
+ }
+
+ @Override
+ public Set getSupportedCommands() {
+ return COMMANDS;
}
@Override
public Set getSupportedFeatures() {
return FEATURES;
}
-//~--- fields ---------------------------------------------------------------
- /** Field description */
- private final GitContext context;
-
- /** Field description */
- private final GitRepositoryHandler handler;
-
- private final LfsBlobStoreFactory lfsBlobStoreFactory;
-
- private final HookContextFactory hookContextFactory;
-
- private final ScmEventBus eventBus;
-
- private final SyncAsyncExecutorProvider executorProvider;
+ @Override
+ public void close() {
+ context.close();
+ }
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java
index 8ffda05ad3..7ff06dd140 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java
@@ -21,19 +21,16 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject;
-import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
-import sonia.scm.event.ScmEventBus;
+import com.google.inject.Injector;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.Repository;
-import sonia.scm.repository.api.HookContextFactory;
-import sonia.scm.web.lfs.LfsBlobStoreFactory;
/**
*
@@ -42,31 +39,20 @@ import sonia.scm.web.lfs.LfsBlobStoreFactory;
@Extension
public class GitRepositoryServiceResolver implements RepositoryServiceResolver {
- private final GitRepositoryHandler handler;
- private final GitRepositoryConfigStoreProvider storeProvider;
- private final LfsBlobStoreFactory lfsBlobStoreFactory;
- private final HookContextFactory hookContextFactory;
- private final ScmEventBus eventBus;
- private final SyncAsyncExecutorProvider executorProvider;
+ private final Injector injector;
+ private final GitContextFactory contextFactory;
@Inject
- public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus, SyncAsyncExecutorProvider executorProvider) {
- this.handler = handler;
- this.storeProvider = storeProvider;
- this.lfsBlobStoreFactory = lfsBlobStoreFactory;
- this.hookContextFactory = hookContextFactory;
- this.eventBus = eventBus;
- this.executorProvider = executorProvider;
+ public GitRepositoryServiceResolver(Injector injector, GitContextFactory contextFactory) {
+ this.injector = injector;
+ this.contextFactory = contextFactory;
}
@Override
public GitRepositoryServiceProvider resolve(Repository repository) {
- GitRepositoryServiceProvider provider = null;
-
if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) {
- provider = new GitRepositoryServiceProvider(handler, repository, storeProvider, lfsBlobStoreFactory, hookContextFactory, eventBus, executorProvider);
+ return new GitRepositoryServiceProvider(injector, contextFactory.create(repository));
}
-
- return provider;
+ return null;
}
}
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java
index 7a9e9f660e..fea4a33f15 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java
@@ -24,7 +24,6 @@
package sonia.scm.repository.spi;
-import org.assertj.core.api.Assertions;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryServiceProviderTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryServiceProviderTest.java
new file mode 100644
index 0000000000..aac6eaef49
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryServiceProviderTest.java
@@ -0,0 +1,74 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.repository.spi;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import sonia.scm.repository.GitRepositoryHandler;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.verify;
+
+@ExtendWith(MockitoExtension.class)
+class GitRepositoryServiceProviderTest {
+
+ @Mock
+ private GitRepositoryHandler handler;
+
+ @Mock
+ private GitContext context;
+
+ @Test
+ void shouldCreatePushCommand() {
+ GitRepositoryServiceProvider provider = createProvider();
+ PushCommand pushCommand = provider.getPushCommand();
+ assertThat(pushCommand).isNotNull().isInstanceOf(GitPushCommand.class);
+ }
+
+ @Test
+ void shouldDelegateCloseToContext() {
+ createProvider().close();
+ verify(context).close();
+ }
+
+ private GitRepositoryServiceProvider createProvider() {
+ return new GitRepositoryServiceProvider(createParentInjector(), context);
+ }
+
+ private Injector createParentInjector() {
+ return Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(GitRepositoryHandler.class).toInstance(handler);
+ }
+ });
+ }
+
+}
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryServiceResolverTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryServiceResolverTest.java
new file mode 100644
index 0000000000..6c2de8b7b6
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryServiceResolverTest.java
@@ -0,0 +1,63 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.repository.spi;
+
+import com.google.inject.Injector;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import sonia.scm.repository.RepositoryTestData;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@ExtendWith(MockitoExtension.class)
+class GitRepositoryServiceResolverTest {
+
+ @Mock
+ private Injector injector;
+
+ @Mock
+ private GitContextFactory contextFactory;
+
+ @InjectMocks
+ private GitRepositoryServiceResolver resolver;
+
+ @Test
+ void shouldCreateRepositoryServiceProvider() {
+ GitRepositoryServiceProvider provider = resolver.resolve(RepositoryTestData.createHeartOfGold("git"));
+ assertThat(provider).isNotNull();
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "hg","svn", "unknown"})
+ void shouldReturnNullForNonGitRepositories(String type) {
+ GitRepositoryServiceProvider provider = resolver.resolve(RepositoryTestData.createHeartOfGold(type));
+ assertThat(provider).isNull();
+ }
+}
diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml
index 4c178bd37c..5daec406b4 100644
--- a/scm-webapp/pom.xml
+++ b/scm-webapp/pom.xml
@@ -222,12 +222,6 @@
${guice.version}
-
- com.google.inject.extensions
- guice-assistedinject
- ${guice.version}
-
-
From 7cb349242c4476ac321886bcf5d67fbccc38ccf3 Mon Sep 17 00:00:00 2001
From: Sebastian Sdorra
Date: Tue, 21 Jul 2020 07:05:26 +0200
Subject: [PATCH 04/46] introduces GitChangesetConverterFactory
This change introduces a GitChangesetConverterFactory to allow injections for the GitChangesetConverter.
---
.../jgit/transport/ScmTransportProtocol.java | 182 ++++--------------
.../git/BaseReceivePackFactory.java | 7 +-
.../git/ScmReceivePackFactory.java | 7 +-
.../scm/repository/GitChangesetConverter.java | 4 +-
.../GitChangesetConverterFactory.java | 40 ++++
.../repository/GitHookChangesetCollector.java | 14 +-
.../AbstractGitIncomingOutgoingCommand.java | 16 +-
.../spi/GitHookChangesetProvider.java | 58 ++----
.../spi/GitHookContextProvider.java | 9 +-
.../repository/spi/GitIncomingCommand.java | 15 +-
.../scm/repository/spi/GitLogCommand.java | 9 +-
.../repository/spi/GitOutgoingCommand.java | 14 +-
.../spi/GitRepositoryServiceProvider.java | 2 +-
.../java/sonia/scm/web/GitReceiveHook.java | 11 +-
.../sonia/scm/web/GitReceivePackFactory.java | 7 +-
.../git/BaseReceivePackFactoryTest.java | 5 +-
.../client/spi/GitCommitCommand.java | 6 +-
.../spi/AbstractRemoteCommandTestBase.java | 21 +-
.../spi/BindTransportProtocolRule.java | 5 +-
.../spi/GitIncomingCommandTest.java | 16 +-
.../spi/GitLogCommandAncestorTest.java | 6 +-
.../scm/repository/spi/GitLogCommandTest.java | 6 +-
.../spi/GitOutgoingCommandTest.java | 7 +-
.../spi/SimpleGitWorkingCopyFactoryTest.java | 3 +-
24 files changed, 179 insertions(+), 291 deletions(-)
create mode 100644 scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverterFactory.java
diff --git a/scm-plugins/scm-git-plugin/src/main/java/org/eclipse/jgit/transport/ScmTransportProtocol.java b/scm-plugins/scm-git-plugin/src/main/java/org/eclipse/jgit/transport/ScmTransportProtocol.java
index 6c5efe722c..61c3013f36 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/org/eclipse/jgit/transport/ScmTransportProtocol.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/org/eclipse/jgit/transport/ScmTransportProtocol.java
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package org.eclipse.jgit.transport;
//~--- non-JDK imports --------------------------------------------------------
@@ -29,200 +29,106 @@ package org.eclipse.jgit.transport;
import com.google.common.collect.ImmutableSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
-
import org.eclipse.jgit.errors.NoRemoteRepositoryException;
-import org.eclipse.jgit.errors.NotSupportedException;
import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryCache;
-
+import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.web.CollectingPackParserListener;
import sonia.scm.web.GitReceiveHook;
-//~--- JDK imports ------------------------------------------------------------
-
import java.io.File;
-
import java.util.Set;
+//~--- JDK imports ------------------------------------------------------------
+
/**
- *
* @author Sebastian Sdorra
*/
-public class ScmTransportProtocol extends TransportProtocol
-{
+public class ScmTransportProtocol extends TransportProtocol {
- /** Field description */
public static final String NAME = "scm";
-
- /** Field description */
private static final Set SCHEMES = ImmutableSet.of(NAME);
- //~--- constructors ---------------------------------------------------------
+ private Provider converterFactory;
+ private Provider hookEventFacadeProvider;
+ private Provider repositoryHandlerProvider;
- /**
- * Constructs ...
- *
- */
- public ScmTransportProtocol() {}
+ public ScmTransportProtocol() {
+ }
- /**
- * Constructs ...
- *
- *
- *
- * @param hookEventFacadeProvider
- *
- * @param repositoryHandlerProvider
- */
@Inject
public ScmTransportProtocol(
+ Provider converterFactory,
Provider hookEventFacadeProvider,
- Provider repositoryHandlerProvider)
- {
+ Provider repositoryHandlerProvider) {
+ this.converterFactory = converterFactory;
this.hookEventFacadeProvider = hookEventFacadeProvider;
this.repositoryHandlerProvider = repositoryHandlerProvider;
}
- //~--- methods --------------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @param uri
- * @param local
- * @param remoteName
- *
- * @return
- */
@Override
- public boolean canHandle(URIish uri, Repository local, String remoteName)
- {
- if ((uri.getPath() == null) || (uri.getPort() > 0)
- || (uri.getUser() != null) || (uri.getPass() != null)
- || (uri.getHost() != null)
- || ((uri.getScheme() != null) &&!getSchemes().contains(uri.getScheme())))
- {
- return false;
- }
-
- return true;
+ public boolean canHandle(URIish uri, Repository local, String remoteName) {
+ return (uri.getPath() != null) && (uri.getPort() <= 0)
+ && (uri.getUser() == null) && (uri.getPass() == null)
+ && (uri.getHost() == null)
+ && ((uri.getScheme() == null) || getSchemes().contains(uri.getScheme()));
}
- /**
- * Method description
- *
- *
- * @param uri
- * @param local
- * @param remoteName
- *
- * @return
- *
- * @throws NotSupportedException
- * @throws TransportException
- */
@Override
- public Transport open(URIish uri, Repository local, String remoteName)
- throws TransportException
- {
+ public Transport open(URIish uri, Repository local, String remoteName) throws TransportException {
File localDirectory = local.getDirectory();
File path = local.getFS().resolve(localDirectory, uri.getPath());
File gitDir = RepositoryCache.FileKey.resolve(path, local.getFS());
- if (gitDir == null)
- {
+ if (gitDir == null) {
throw new NoRemoteRepositoryException(uri, JGitText.get().notFound);
}
- //J-
return new TransportLocalWithHooks(
+ converterFactory.get(),
hookEventFacadeProvider.get(),
repositoryHandlerProvider.get(),
local, uri, gitDir
);
- //J+
}
- //~--- get methods ----------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @return
- */
@Override
- public String getName()
- {
+ public String getName() {
return NAME;
}
- /**
- * Method description
- *
- *
- * @return
- */
@Override
- public Set getSchemes()
- {
+ public Set getSchemes() {
return SCHEMES;
}
- //~--- inner classes --------------------------------------------------------
+ private static class TransportLocalWithHooks extends TransportLocal {
- /**
- * Class description
- *
- *
- * @version Enter version here..., 13/05/19
- * @author Enter your name here...
- */
- private static class TransportLocalWithHooks extends TransportLocal
- {
+ private final GitChangesetConverterFactory converterFactory;
+ private final GitRepositoryHandler handler;
+ private final HookEventFacade hookEventFacade;
- /**
- * Constructs ...
- *
- *
- *
- * @param hookEventFacade
- * @param handler
- * @param local
- * @param uri
- * @param gitDir
- */
- public TransportLocalWithHooks(HookEventFacade hookEventFacade,
- GitRepositoryHandler handler, Repository local, URIish uri, File gitDir)
- {
+ public TransportLocalWithHooks(
+ GitChangesetConverterFactory converterFactory,
+ HookEventFacade hookEventFacade,
+ GitRepositoryHandler handler,
+ Repository local, URIish uri, File gitDir) {
super(local, uri, gitDir);
+ this.converterFactory = converterFactory;
this.hookEventFacade = hookEventFacade;
this.handler = handler;
}
- //~--- methods ------------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @param dst
- *
- * @return
- */
@Override
- ReceivePack createReceivePack(Repository dst)
- {
+ ReceivePack createReceivePack(Repository dst) {
ReceivePack pack = new ReceivePack(dst);
- if ((hookEventFacade != null) && (handler != null))
- {
- GitReceiveHook hook = new GitReceiveHook(hookEventFacade, handler);
+ if ((hookEventFacade != null) && (handler != null) && (converterFactory != null)) {
+ GitReceiveHook hook = new GitReceiveHook(converterFactory, hookEventFacade, handler);
pack.setPreReceiveHook(hook);
pack.setPostReceiveHook(hook);
@@ -232,22 +138,6 @@ public class ScmTransportProtocol extends TransportProtocol
return pack;
}
-
- //~--- fields -------------------------------------------------------------
-
- /** Field description */
- private GitRepositoryHandler handler;
-
- /** Field description */
- private HookEventFacade hookEventFacade;
}
-
- //~--- fields ---------------------------------------------------------------
-
- /** Field description */
- private Provider hookEventFacadeProvider;
-
- /** Field description */
- private Provider repositoryHandlerProvider;
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/BaseReceivePackFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/BaseReceivePackFactory.java
index 9b18991d1a..028e91009d 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/BaseReceivePackFactory.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/BaseReceivePackFactory.java
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.protocolcommand.git;
import org.eclipse.jgit.lib.Repository;
@@ -29,6 +29,7 @@ import org.eclipse.jgit.transport.ReceivePack;
import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
+import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.web.CollectingPackParserListener;
@@ -39,9 +40,9 @@ public abstract class BaseReceivePackFactory implements ReceivePackFactory
private final GitRepositoryHandler handler;
private final GitReceiveHook hook;
- protected BaseReceivePackFactory(GitRepositoryHandler handler, HookEventFacade hookEventFacade) {
+ protected BaseReceivePackFactory(GitChangesetConverterFactory converterFactory, GitRepositoryHandler handler, HookEventFacade hookEventFacade) {
this.handler = handler;
- this.hook = new GitReceiveHook(hookEventFacade, handler);
+ this.hook = new GitReceiveHook(converterFactory, hookEventFacade, handler);
}
@Override
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/ScmReceivePackFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/ScmReceivePackFactory.java
index f15ccfcf99..a999a57d4d 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/ScmReceivePackFactory.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/ScmReceivePackFactory.java
@@ -21,21 +21,22 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.protocolcommand.git;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.ReceivePack;
import sonia.scm.protocolcommand.RepositoryContext;
+import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.spi.HookEventFacade;
public class ScmReceivePackFactory extends BaseReceivePackFactory {
@Inject
- public ScmReceivePackFactory(GitRepositoryHandler handler, HookEventFacade hookEventFacade) {
- super(handler, hookEventFacade);
+ public ScmReceivePackFactory(GitChangesetConverterFactory converterFactory, GitRepositoryHandler handler, HookEventFacade hookEventFacade) {
+ super(converterFactory, handler, hookEventFacade);
}
@Override
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java
index 0f08f54aaa..3b8e6dadad 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java
@@ -67,7 +67,7 @@ public class GitChangesetConverter implements Closeable
*
* @param repository
*/
- public GitChangesetConverter(org.eclipse.jgit.lib.Repository repository)
+ GitChangesetConverter(org.eclipse.jgit.lib.Repository repository)
{
this(repository, null);
}
@@ -79,7 +79,7 @@ public class GitChangesetConverter implements Closeable
* @param repository
* @param revWalk
*/
- public GitChangesetConverter(org.eclipse.jgit.lib.Repository repository,
+ GitChangesetConverter(org.eclipse.jgit.lib.Repository repository,
RevWalk revWalk)
{
this.repository = repository;
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverterFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverterFactory.java
new file mode 100644
index 0000000000..c5eb6e312a
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverterFactory.java
@@ -0,0 +1,40 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.repository;
+
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+public class GitChangesetConverterFactory {
+
+ public GitChangesetConverter create(Repository repository) {
+ return new GitChangesetConverter(repository);
+ }
+
+ public GitChangesetConverter create(Repository repository, RevWalk revWalk) {
+ return new GitChangesetConverter(repository, revWalk);
+ }
+
+}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHookChangesetCollector.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHookChangesetCollector.java
index 3071f590bc..5d7de5ce27 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHookChangesetCollector.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHookChangesetCollector.java
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
@@ -72,9 +72,10 @@ public class GitHookChangesetCollector
* @param rpack
* @param receiveCommands
*/
- public GitHookChangesetCollector(ReceivePack rpack,
+ public GitHookChangesetCollector(GitChangesetConverterFactory converterFactory, ReceivePack rpack,
List receiveCommands)
{
+ this.converterFactory = converterFactory;
this.rpack = rpack;
this.receiveCommands = receiveCommands;
this.listener = CollectingPackParserListener.get(rpack);
@@ -100,14 +101,14 @@ public class GitHookChangesetCollector
try
{
walk = rpack.getRevWalk();
- converter = new GitChangesetConverter(repository, walk);
+ converter = converterFactory.create(repository, walk);
for (ReceiveCommand rc : receiveCommands)
{
String ref = rc.getRefName();
-
+
logger.trace("handle receive command, type={}, ref={}, result={}", rc.getType(), ref, rc.getResult());
-
+
if (rc.getType() == ReceiveCommand.Type.DELETE)
{
logger.debug("skip delete of ref {}", ref);
@@ -130,7 +131,7 @@ public class GitHookChangesetCollector
builder.append(rc.getType()).append(", ref=");
builder.append(rc.getRefName()).append(", result=");
builder.append(rc.getResult());
-
+
logger.error(builder.toString(), ex);
}
}
@@ -222,5 +223,6 @@ public class GitHookChangesetCollector
private final List receiveCommands;
+ private final GitChangesetConverterFactory converterFactory;
private final ReceivePack rpack;
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java
index 1b03a5a55e..94b1451d68 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java
@@ -36,6 +36,7 @@ import org.eclipse.jgit.revwalk.RevWalk;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitChangesetConverter;
+import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
@@ -58,18 +59,10 @@ public abstract class AbstractGitIncomingOutgoingCommand
/** Field description */
private static final String REMOTE_REF_PREFIX = "refs/remote/scm/%s/";
- //~--- constructors ---------------------------------------------------------
-
- /**
- * Constructs ...
- *
- * @param handler
- * @param context
- */
- AbstractGitIncomingOutgoingCommand(GitRepositoryHandler handler, GitContext context)
- {
+ AbstractGitIncomingOutgoingCommand(GitContext context, GitRepositoryHandler handler, GitChangesetConverterFactory converterFactory) {
super(context);
this.handler = handler;
+ this.converterFactory = converterFactory;
}
//~--- methods --------------------------------------------------------------
@@ -132,7 +125,7 @@ public abstract class AbstractGitIncomingOutgoingCommand
try
{
walk = new RevWalk(git.getRepository());
- converter = new GitChangesetConverter(git.getRepository(), walk);
+ converter = converterFactory.create(git.getRepository(), walk);
org.eclipse.jgit.api.LogCommand log = git.log();
@@ -203,4 +196,5 @@ public abstract class AbstractGitIncomingOutgoingCommand
/** Field description */
private GitRepositoryHandler handler;
+ private final GitChangesetConverterFactory converterFactory;
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookChangesetProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookChangesetProvider.java
index 3280e2c5c4..1dbe652371 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookChangesetProvider.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookChangesetProvider.java
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
@@ -29,6 +29,7 @@ package sonia.scm.repository.spi;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.ReceivePack;
+import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitHookChangesetCollector;
//~--- JDK imports ------------------------------------------------------------
@@ -39,56 +40,27 @@ import java.util.List;
*
* @author Sebastian Sdorra
*/
-public class GitHookChangesetProvider implements HookChangesetProvider
-{
+public class GitHookChangesetProvider implements HookChangesetProvider {
- /**
- * Constructs ...
- *
- *
- * @param receivePack
- * @param receiveCommands
- */
- public GitHookChangesetProvider(ReceivePack receivePack,
- List receiveCommands)
- {
+ private final GitChangesetConverterFactory converterFactory;
+ private final ReceivePack receivePack;
+ private final List receiveCommands;
+
+ private HookChangesetResponse response;
+
+ public GitHookChangesetProvider(GitChangesetConverterFactory converterFactory, ReceivePack receivePack,
+ List receiveCommands) {
+ this.converterFactory = converterFactory;
this.receivePack = receivePack;
this.receiveCommands = receiveCommands;
}
- //~--- methods --------------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @param request
- *
- * @return
- */
@Override
- public synchronized HookChangesetResponse handleRequest(
- HookChangesetRequest request)
- {
- if (response == null)
- {
- GitHookChangesetCollector collector =
- new GitHookChangesetCollector(receivePack, receiveCommands);
-
+ public synchronized HookChangesetResponse handleRequest(HookChangesetRequest request) {
+ if (response == null) {
+ GitHookChangesetCollector collector = new GitHookChangesetCollector(converterFactory, receivePack, receiveCommands);
response = new HookChangesetResponse(collector.collectChangesets());
}
-
return response;
}
-
- //~--- fields ---------------------------------------------------------------
-
- /** Field description */
- private List receiveCommands;
-
- /** Field description */
- private ReceivePack receivePack;
-
- /** Field description */
- private HookChangesetResponse response;
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookContextProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookContextProvider.java
index 8e642a18ce..95843970e4 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookContextProvider.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookContextProvider.java
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
@@ -29,6 +29,7 @@ package sonia.scm.repository.spi;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.ReceivePack;
+import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.api.GitHookBranchProvider;
import sonia.scm.repository.api.GitHookMessageProvider;
import sonia.scm.repository.api.HookBranchProvider;
@@ -63,12 +64,12 @@ public class GitHookContextProvider extends HookContextProvider
* @param receivePack git receive pack
* @param receiveCommands received commands
*/
- public GitHookContextProvider(ReceivePack receivePack,
- List receiveCommands)
+ public GitHookContextProvider(GitChangesetConverterFactory converterFactory, ReceivePack receivePack,
+ List receiveCommands)
{
this.receivePack = receivePack;
this.receiveCommands = receiveCommands;
- this.changesetProvider = new GitHookChangesetProvider(receivePack,
+ this.changesetProvider = new GitHookChangesetProvider(converterFactory, receivePack,
receiveCommands);
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitIncomingCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitIncomingCommand.java
index 638983b6f1..205d9e9e3f 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitIncomingCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitIncomingCommand.java
@@ -29,6 +29,7 @@ package sonia.scm.repository.spi;
import org.eclipse.jgit.api.LogCommand;
import org.eclipse.jgit.lib.ObjectId;
import sonia.scm.repository.ChangesetPagingResult;
+import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitRepositoryHandler;
import javax.inject.Inject;
@@ -41,19 +42,11 @@ import java.io.IOException;
* @author Sebastian Sdorra
*/
public class GitIncomingCommand extends AbstractGitIncomingOutgoingCommand
- implements IncomingCommand
-{
+ implements IncomingCommand {
- /**
- * Constructs ...
- *
- * @param handler
- * @param context
- */
@Inject
- GitIncomingCommand(GitRepositoryHandler handler, GitContext context)
- {
- super(handler, context);
+ GitIncomingCommand(GitContext context, GitRepositoryHandler handler, GitChangesetConverterFactory converterFactory) {
+ super(context, handler, converterFactory);
}
//~--- get methods ----------------------------------------------------------
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java
index b1205de975..bd50d0fb07 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java
@@ -43,6 +43,7 @@ import sonia.scm.NotFoundException;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitChangesetConverter;
+import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.util.IOUtil;
@@ -71,6 +72,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
private static final Logger logger =
LoggerFactory.getLogger(GitLogCommand.class);
public static final String REVISION = "Revision";
+ private final GitChangesetConverterFactory converterFactory;
//~--- constructors ---------------------------------------------------------
@@ -82,9 +84,10 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
*
*/
@Inject
- GitLogCommand(GitContext context)
+ GitLogCommand(GitContext context, GitChangesetConverterFactory converterFactory)
{
super(context);
+ this.converterFactory = converterFactory;
}
//~--- get methods ----------------------------------------------------------
@@ -122,7 +125,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
if (commit != null)
{
- converter = new GitChangesetConverter(gr, revWalk);
+ converter = converterFactory.create(gr, revWalk);
if (isBranchRequested(request)) {
String branch = request.getBranch();
@@ -233,7 +236,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
revWalk = new RevWalk(repository);
- converter = new GitChangesetConverter(repository, revWalk);
+ converter = converterFactory.create(repository, revWalk);
if (!Strings.isNullOrEmpty(request.getPath())) {
revWalk.setTreeFilter(
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java
index bfb319df10..30192d4297 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java
@@ -29,6 +29,7 @@ package sonia.scm.repository.spi;
import org.eclipse.jgit.api.LogCommand;
import org.eclipse.jgit.lib.ObjectId;
import sonia.scm.repository.ChangesetPagingResult;
+import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitRepositoryHandler;
import javax.inject.Inject;
@@ -41,19 +42,12 @@ import java.io.IOException;
* @author Sebastian Sdorra
*/
public class GitOutgoingCommand extends AbstractGitIncomingOutgoingCommand
- implements OutgoingCommand
-{
+ implements OutgoingCommand {
- /**
- * Constructs ...
- *
- * @param handler
- * @param context
- */
@Inject
- GitOutgoingCommand(GitRepositoryHandler handler, GitContext context)
+ GitOutgoingCommand(GitContext context, GitRepositoryHandler handler, GitChangesetConverterFactory converterFactory)
{
- super(handler, context);
+ super(context, handler, converterFactory);
}
//~--- get methods ----------------------------------------------------------
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java
index 75b0f97b3f..fae69a47cf 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java
@@ -117,7 +117,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
@Override
public LogCommand getLogCommand() {
- return new GitLogCommand(context);
+ return commandInjector.getInstance(GitLogCommand.class);
}
@Override
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceiveHook.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceiveHook.java
index c7b323224c..4c127ca9af 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceiveHook.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceiveHook.java
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.web;
//~--- non-JDK imports --------------------------------------------------------
@@ -34,6 +34,7 @@ import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.ReceivePack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.spi.GitHookContextProvider;
@@ -66,9 +67,10 @@ public class GitReceiveHook implements PreReceiveHook, PostReceiveHook
* @param hookEventFacade
* @param handler
*/
- public GitReceiveHook(HookEventFacade hookEventFacade,
- GitRepositoryHandler handler)
+ public GitReceiveHook(GitChangesetConverterFactory converterFactory, HookEventFacade hookEventFacade,
+ GitRepositoryHandler handler)
{
+ this.converterFactory = converterFactory;
this.hookEventFacade = hookEventFacade;
this.handler = handler;
}
@@ -122,7 +124,7 @@ public class GitReceiveHook implements PreReceiveHook, PostReceiveHook
logger.trace("resolved repository to {}", repositoryId);
- GitHookContextProvider context = new GitHookContextProvider(rpack,
+ GitHookContextProvider context = new GitHookContextProvider(converterFactory, rpack,
receiveCommands);
hookEventFacade.handle(repositoryId).fireHookEvent(type, context);
@@ -188,6 +190,7 @@ public class GitReceiveHook implements PreReceiveHook, PostReceiveHook
/** Field description */
private GitRepositoryHandler handler;
+ private final GitChangesetConverterFactory converterFactory;
/** Field description */
private HookEventFacade hookEventFacade;
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java
index b4b43ea5b5..dc18189da9 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.web;
//~--- non-JDK imports --------------------------------------------------------
@@ -34,6 +34,7 @@ import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
import sonia.scm.protocolcommand.git.BaseReceivePackFactory;
+import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.spi.HookEventFacade;
@@ -53,8 +54,8 @@ public class GitReceivePackFactory extends BaseReceivePackFactory wrapped;
@Inject
- public GitReceivePackFactory(GitRepositoryHandler handler, HookEventFacade hookEventFacade) {
- super(handler, hookEventFacade);
+ public GitReceivePackFactory(GitChangesetConverterFactory converterFactory, GitRepositoryHandler handler, HookEventFacade hookEventFacade) {
+ super(converterFactory, handler, hookEventFacade);
this.wrapped = new DefaultReceivePackFactory();
}
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/BaseReceivePackFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/BaseReceivePackFactoryTest.java
index 8fa2ecddaa..d6de6a0780 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/BaseReceivePackFactoryTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/BaseReceivePackFactoryTest.java
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.protocolcommand.git;
import org.eclipse.jgit.api.Git;
@@ -38,6 +38,7 @@ import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
+import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitConfig;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.web.CollectingPackParserListener;
@@ -82,7 +83,7 @@ public class BaseReceivePackFactoryTest {
ReceivePack receivePack = new ReceivePack(repository);
when(wrappedReceivePackFactory.create(request, repository)).thenReturn(receivePack);
- factory = new BaseReceivePackFactory
+
+
+
+ org.bouncycastle
+ bcpg-jdk15on
+ ${bouncycastle.version}
+
+
+
+ org.bouncycastle
+ bcprov-jdk15on
+ ${bouncycastle.version}
+
+
+
+ org.bouncycastle
+ bcpkix-jdk15on
+ ${bouncycastle.version}
+
+
@@ -899,6 +919,7 @@
4.2.32.3.36.1.5.Final
+ 1.651.6.2
diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml
index 5daec406b4..eb83eaeb11 100644
--- a/scm-webapp/pom.xml
+++ b/scm-webapp/pom.xml
@@ -114,6 +114,23 @@
${jjwt.version}
+
+
+
+ org.bouncycastle
+ bcpg-jdk15on
+
+
+
+ org.bouncycastle
+ bcprov-jdk15on
+
+
+
+ org.bouncycastle
+ bcpkix-jdk15on
+
+
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/GPGException.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/GPGException.java
new file mode 100644
index 0000000000..da278f03da
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/GPGException.java
@@ -0,0 +1,35 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+public class GPGException extends RuntimeException {
+ GPGException(String message) {
+ super(message);
+ }
+
+ GPGException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/Keys.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/Keys.java
new file mode 100644
index 0000000000..7ecbd1d49d
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/Keys.java
@@ -0,0 +1,86 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.gpg.keybox.PublicKeyRingBlob;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPObjectFactory;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
+import org.bouncycastle.openpgp.PGPUtil;
+import org.bouncycastle.openpgp.jcajce.JcaPGPPublicKeyRing;
+import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator;
+import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
+import sonia.scm.security.PublicKey;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+final class Keys {
+
+ private static final KeyFingerPrintCalculator calculator = new JcaKeyFingerprintCalculator();
+
+ private Keys() {}
+
+ static String resolveIdFromKey(String rawKey) throws IOException, PGPException {
+ List keys = collectKeys(rawKey);
+ if (keys.size() > 1) {
+ keys = keys.stream().filter(PGPPublicKey::isMasterKey).collect(Collectors.toList());
+ }
+ if (keys.isEmpty()) {
+ throw new IllegalArgumentException("found multiple keys, but no master keys");
+ }
+ if (keys.size() > 1) {
+ throw new IllegalArgumentException("found multiple master keys");
+ }
+
+ PGPPublicKey pgpPublicKey = keys.get(0);
+ return createId(pgpPublicKey);
+ }
+
+ private static String createId(PGPPublicKey pgpPublicKey) {
+ return "0x" + Long.toHexString(pgpPublicKey.getKeyID()).toUpperCase(Locale.ENGLISH);
+ }
+
+ private static List collectKeys(String rawKey) throws IOException, PGPException {
+ List publicKeys = new ArrayList<>();
+ InputStream decoderStream = PGPUtil.getDecoderStream(new ByteArrayInputStream(rawKey.getBytes(StandardCharsets.UTF_8)));
+ PGPPublicKeyRingCollection collection = new PGPPublicKeyRingCollection(decoderStream, calculator);
+ for (PGPPublicKeyRing pgpPublicKeys : collection) {
+ for (PGPPublicKey pgpPublicKey : pgpPublicKeys) {
+ publicKeys.add(pgpPublicKey);
+ }
+ }
+ return publicKeys;
+ }
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java
new file mode 100644
index 0000000000..394e657e59
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java
@@ -0,0 +1,86 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.apache.shiro.SecurityUtils;
+import org.bouncycastle.openpgp.PGPException;
+import sonia.scm.security.PublicKey;
+import sonia.scm.store.DataStore;
+import sonia.scm.store.DataStoreFactory;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+@Singleton
+public class PublicKeyStore {
+
+ private static final String STORE_NAME = "gpg_public_keys";
+
+ private final DataStore store;
+ private final Supplier currentUserSupplier;
+
+ @Inject
+ public PublicKeyStore(DataStoreFactory dataStoreFactory) {
+ this(
+ dataStoreFactory.withType(RawGpgKey.class).withName(STORE_NAME).build(),
+ () -> SecurityUtils.getSubject().getPrincipal().toString()
+ );
+ }
+
+ @VisibleForTesting
+ PublicKeyStore(DataStore store, Supplier currentUserSupplier) {
+ this.store = store;
+ this.currentUserSupplier = currentUserSupplier;
+ }
+
+ public RawGpgKey add(String displayName, String rawKey) {
+ try {
+ String id = Keys.resolveIdFromKey(rawKey);
+ RawGpgKey key = new RawGpgKey(id, displayName, currentUserSupplier.get(), rawKey, Instant.now());
+
+ store.put(id, key);
+
+ return key;
+ } catch (IOException | PGPException e) {
+ throw new GPGException("failed to resolve id from gpg key");
+ }
+ }
+
+ public Optional findById(String id) {
+ return store.getOptional(id);
+ }
+
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKey.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKey.java
new file mode 100644
index 0000000000..5b098c5af3
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKey.java
@@ -0,0 +1,74 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import sonia.scm.xml.XmlInstantAdapter;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+import java.time.Instant;
+import java.util.Objects;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@XmlAccessorType(XmlAccessType.FIELD)
+public class RawGpgKey {
+
+ private String id;
+ private String displayName;
+ private String owner;
+ private String raw;
+
+ @XmlJavaTypeAdapter(XmlInstantAdapter.class)
+ private Instant created;
+
+ RawGpgKey(String id) {
+ this.id = id;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ RawGpgKey that = (RawGpgKey) o;
+ return Objects.equals(id, that.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGTestHelper.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGTestHelper.java
new file mode 100644
index 0000000000..4608aace5c
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGTestHelper.java
@@ -0,0 +1,44 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+import com.google.common.io.Resources;
+
+import java.io.IOException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+
+final class GPGTestHelper {
+
+ private GPGTestHelper() {
+ }
+
+ @SuppressWarnings("UnstableApiUsage")
+ static String readKey(String key) throws IOException {
+ URL resource = Resources.getResource("sonia/scm/security/gpg/" + key);
+ return Resources.toString(resource, StandardCharsets.UTF_8);
+ }
+
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysTest.java
new file mode 100644
index 0000000000..765674d0f2
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysTest.java
@@ -0,0 +1,49 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+import org.bouncycastle.openpgp.PGPException;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static sonia.scm.security.gpg.GPGTestHelper.readKey;
+
+class KeysTest {
+
+ @Test
+ void shouldResolveId() throws IOException, PGPException {
+ String rawPublicKey = readKey("single.asc");
+ assertThat(Keys.resolveIdFromKey(rawPublicKey)).isEqualTo("0x975922F193B07D6E");
+ }
+
+ @Test
+ void shouldResolveIdFromMasterKey() throws IOException, PGPException {
+ String rawPublicKey = readKey("subkeys.asc");
+ assertThat(Keys.resolveIdFromKey(rawPublicKey)).isEqualTo("0x13B13D4C8A9350A1");
+ }
+
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java
new file mode 100644
index 0000000000..3c26256247
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java
@@ -0,0 +1,67 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import sonia.scm.store.InMemoryDataStore;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class PublicKeyStoreTest {
+
+ private PublicKeyStore keyStore;
+
+ @BeforeEach
+ void setUpKeyStore() {
+ keyStore = new PublicKeyStore(new InMemoryDataStore<>(), () -> "trillian");
+ }
+
+ @Test
+ void shouldReturnStoredKey() throws IOException {
+ String rawKey = GPGTestHelper.readKey("single.asc");
+ Instant now = Instant.now();
+
+ RawGpgKey key = keyStore.add("SCM Package Key", rawKey);
+ assertThat(key.getId()).isEqualTo("0x975922F193B07D6E");
+ assertThat(key.getDisplayName()).isEqualTo("SCM Package Key");
+ assertThat(key.getOwner()).isEqualTo("trillian");
+ assertThat(key.getCreated()).isAfterOrEqualTo(now);
+ assertThat(key.getRaw()).isEqualTo(rawKey);
+ }
+
+ @Test
+ void shouldFindStoredKeyById() throws IOException {
+ String rawKey = GPGTestHelper.readKey("single.asc");
+ keyStore.add("SCM Package Key", rawKey);
+ Optional key = keyStore.findById("0x975922F193B07D6E");
+ assertThat(key).isPresent();
+ }
+
+}
diff --git a/scm-webapp/src/test/resources/sonia/scm/security/gpg/single.asc b/scm-webapp/src/test/resources/sonia/scm/security/gpg/single.asc
new file mode 100644
index 0000000000..8c06de15d9
--- /dev/null
+++ b/scm-webapp/src/test/resources/sonia/scm/security/gpg/single.asc
@@ -0,0 +1,30 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQINBF69VQEBEADcnNUucubBBn0nEubrNV3SWh1CbiUyhLU6TEKSUbyB2gDcOrMR
+4wAeqK0ar4cloIpS5YEtjT/qVpERzabbJe0NSfCwYblpdfoA0idcvi7Ssnczfr/i
+1cF0gqmAjoDqAaSk7xHa/mxiEwAUAXGDWu4pCksT8gHDDx/7lIkHvPZs+VGyMM/O
+NVc3QET0JMuKhBYJafSJkdUl3qjX9N6ykETQIbxSv0YLjZuAzQJggLoiMWZtDkrU
+KuXh/bJja2xnzj9XtMmpkrIRAX1VYLlI7sJ17b1Tv/yIvaZ1akccRSFtx/kqcM86
+LSO77E82EfLhuVeVCvzxzgfrYHVVX6oFhAEzKUI0TRT453yGZuC5j91HXP3VuXjH
+opAODeQLbDfcWPH8joegDZBuK1dQRvVcyh5CMSAWHw9vgXKergrMHVB4EVIqMMir
+Q29KfbSuhA5D+xLxPjphbDsMIcLMy0ADd7N0ydmpt2x7ES3Sx3iTssibBYQB731Z
+DQCgxy8mdL4MSCNUkDziGyqK+cI9/jRehiOimFsnQIDqQ1hOQBw7M21lvGlbn1IA
+iKosi9rb+tUtnNT2d3byjvjZzMj1xoJeOs9i+xEDu1224xEEfJIixfSLLW2T8Qdb
+6/a5XGENQB9ZfcW0CrK+V+bHLKXkY2MG9mAL3KEgmDDqydQTwOGNFzJHpwARAQAB
+tE9TQ00gUGFja2FnZXMgKHNpZ25pbmcga2V5IGZvciBwYWNrYWdlcy5zY20tbWFu
+YWdlci5vcmcpIDxzY20tdGVhbUBjbG91ZG9ndS5jb20+iQJOBBMBCgA4FiEEI9Ji
+WyNeJaRxmHWil1ki8ZOwfW4FAl69VQECGy8FCwkIBwMFFQoJCAsFFgIDAQACHgEC
+F4AACgkQl1ki8ZOwfW49vg/9EYZSEejdfuzLWcC1M8C9lyausvB+SAI7fEcnD4do
+w6WEdnPTus5aAnr1qOncH3aJpjwqfIpuCMdS94i9jgLJTLaQ8S2WegLFVhDQvC7v
+Q6ZieOUAYVWJx2Klq2OT1MVJPEzskV3QtFBTaHmuseJrGvH0Waw26MGw8MiAPyES
+oZGdcULZBwpr8nazqcFXFuDxMFr1Y28sEzW/ntfScLnIVIVXAWaAXq/4dtB1cIIc
+KKsszkM018HdEPSf99ry/nqiwGkOBqMUiEM9+VIMuJRs+BSvT9ETM0yx21fYV2Jj
+YG20ahsd1tRFwYLLyzwukT9KUBydZ2RZP+L0gkC+WrfMxvreQxP7d8PH8aF2Ii1e
+SpLs91h97tXq+ucp6YyGTEsVnajQeGSA0mX/AhOe3swBNZ08vuhSWkKjKnOXqR4h
+IyJaJGAuo6vd+GzdAu/9MxWZQZTWERauofxLTzESwJl7WfTgEFvF+7hNCkQmUA7r
+oGc8ahEvuGCZG2MtfBPSPL51FifDlO+G0rifWqHuocZWdBX6fcmt+SYb8SHaG0cu
+JP35uWVGuva+Bw73+S21xU3yIjt8bTkZpIuHO9xhivXIOVR3jVhB1V6KrI+jlZl/
+DmQc8MI3+Ez6Hh3kVQjokL1W/u/gg8XumCmc+4hq2QIOSF/ODUMg0b1nne4msLc/
+H1Q=
+=5/Nh
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/scm-webapp/src/test/resources/sonia/scm/security/gpg/subkeys.asc b/scm-webapp/src/test/resources/sonia/scm/security/gpg/subkeys.asc
new file mode 100644
index 0000000000..eaacc24062
--- /dev/null
+++ b/scm-webapp/src/test/resources/sonia/scm/security/gpg/subkeys.asc
@@ -0,0 +1,122 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+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
+=dKpI
+-----END PGP PUBLIC KEY BLOCK-----
From 37f3c15b84a7f45b1e4fa4c0b39d99f071066ca3 Mon Sep 17 00:00:00 2001
From: Sebastian Sdorra
Date: Fri, 24 Jul 2020 11:57:06 +0200
Subject: [PATCH 07/46] fixed npe on build
---
pom.xml | 4 ++--
scm-webapp/pom.xml | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/pom.xml b/pom.xml
index 11b51fb99c..f7eff5138d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -615,7 +615,7 @@
org.apache.maven.pluginsmaven-enforcer-plugin
- 3.0.0-M1
+ 3.0.0-M3enforce-java
@@ -659,7 +659,7 @@
org.codehaus.mojoextra-enforcer-rules
- 1.0-beta-7
+ 1.3
diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml
index eb83eaeb11..28bf55743f 100644
--- a/scm-webapp/pom.xml
+++ b/scm-webapp/pom.xml
@@ -661,7 +661,7 @@
org.basepom.mavenduplicate-finder-maven-plugin
- 1.3.0
+ 1.4.0default
From 13326d62535f7e776449d0fbe8237bbfd96dfa1f Mon Sep 17 00:00:00 2001
From: Sebastian Sdorra
Date: Fri, 24 Jul 2020 11:58:28 +0200
Subject: [PATCH 08/46] set since tag for gpg stuff to 2.4.0, rename
Signature.key to Signature.keyId and added DummyGPG for testing
---
.../java/sonia/scm/repository/Changeset.java | 6 +-
.../java/sonia/scm/repository/Signature.java | 4 +-
.../src/main/java/sonia/scm/security/GPG.java | 2 +-
.../java/sonia/scm/security/PrivateKey.java | 2 +-
.../java/sonia/scm/security/PublicKey.java | 2 +-
.../java/sonia/scm/security/gpg/DummyGPG.java | 58 +++++++++++++++++++
.../sonia/scm/security/gpg/GPGModule.java | 37 ++++++++++++
.../java/sonia/scm/security/gpg/Keys.java | 5 --
.../scm/security/gpg/PublicKeyStore.java | 7 ---
9 files changed, 103 insertions(+), 20 deletions(-)
create mode 100644 scm-webapp/src/main/java/sonia/scm/security/gpg/DummyGPG.java
create mode 100644 scm-webapp/src/main/java/sonia/scm/security/gpg/GPGModule.java
diff --git a/scm-core/src/main/java/sonia/scm/repository/Changeset.java b/scm-core/src/main/java/sonia/scm/repository/Changeset.java
index 4fb6aa4d6b..914fd8f90a 100644
--- a/scm-core/src/main/java/sonia/scm/repository/Changeset.java
+++ b/scm-core/src/main/java/sonia/scm/repository/Changeset.java
@@ -355,7 +355,7 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
/**
* Sets a collection of signatures which belong to this changeset.
* @param signatures collection of signatures
- * @since 2.3.0
+ * @since 2.4.0
*/
public void setSignatures(Collection signatures) {
this.signatures = new ArrayList<>(signatures);
@@ -364,7 +364,7 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
/**
* Returns a immutable list of signatures.
* @return signatures
- * @since 2.3.0
+ * @since 2.4.0
*/
public List getSignatures() {
return Collections.unmodifiableList(signatures);
@@ -373,7 +373,7 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
/**
* Adds a signature to the list of signatures.
* @param signature
- * @since 2.3.0
+ * @since 2.4.0
*/
public void addSignature(Signature signature) {
signatures.add(signature);
diff --git a/scm-core/src/main/java/sonia/scm/repository/Signature.java b/scm-core/src/main/java/sonia/scm/repository/Signature.java
index 3e67613447..b077793f7b 100644
--- a/scm-core/src/main/java/sonia/scm/repository/Signature.java
+++ b/scm-core/src/main/java/sonia/scm/repository/Signature.java
@@ -31,14 +31,14 @@ import java.util.Optional;
/**
* Signature is the output of a signature verification.
- * @since 2.3.0
+ * @since 2.4.0
*/
@Value
public class Signature implements Serializable {
private static final long serialVersionUID = 1L;
- private final String key;
+ private final String keyId;
private final String type;
private final boolean verified;
private final String owner;
diff --git a/scm-core/src/main/java/sonia/scm/security/GPG.java b/scm-core/src/main/java/sonia/scm/security/GPG.java
index c2cd6b8177..2fa773c906 100644
--- a/scm-core/src/main/java/sonia/scm/security/GPG.java
+++ b/scm-core/src/main/java/sonia/scm/security/GPG.java
@@ -29,7 +29,7 @@ import java.util.Optional;
/**
* Allows signing and verification using gpg.
*
- * @since 2.3.0
+ * @since 2.4.0
*/
public interface GPG {
diff --git a/scm-core/src/main/java/sonia/scm/security/PrivateKey.java b/scm-core/src/main/java/sonia/scm/security/PrivateKey.java
index dc62a4ca74..b1d12582a7 100644
--- a/scm-core/src/main/java/sonia/scm/security/PrivateKey.java
+++ b/scm-core/src/main/java/sonia/scm/security/PrivateKey.java
@@ -29,7 +29,7 @@ import java.io.InputStream;
/**
* Can be used to create signatures of data.
- * @since 2.3.0
+ * @since 2.4.0
*/
public interface PrivateKey {
diff --git a/scm-core/src/main/java/sonia/scm/security/PublicKey.java b/scm-core/src/main/java/sonia/scm/security/PublicKey.java
index 30e3fe1072..734b19afc1 100644
--- a/scm-core/src/main/java/sonia/scm/security/PublicKey.java
+++ b/scm-core/src/main/java/sonia/scm/security/PublicKey.java
@@ -31,7 +31,7 @@ import java.util.Optional;
/**
* The public key can be used to verify signatures.
*
- * @since 2.3.0
+ * @since 2.4.0
*/
public interface PublicKey {
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/DummyGPG.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/DummyGPG.java
new file mode 100644
index 0000000000..79c6e516bb
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/DummyGPG.java
@@ -0,0 +1,58 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+import sonia.scm.security.GPG;
+import sonia.scm.security.PrivateKey;
+import sonia.scm.security.PublicKey;
+
+import java.util.Collections;
+import java.util.Optional;
+
+/**
+ * Dummy implementation of {@link GPG} should be replaced soon.
+ */
+public class DummyGPG implements GPG {
+
+ @Override
+ public String findPublicKeyId(byte[] signature) {
+ return "unknown";
+ }
+
+ @Override
+ public Optional findPublicKey(String id) {
+ return Optional.empty();
+ }
+
+ @Override
+ public Iterable findPublicKeysByUsername(String username) {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public PrivateKey getPrivateKey() {
+ throw new UnsupportedOperationException("getPrivateKey is not yet implemented");
+ }
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/GPGModule.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/GPGModule.java
new file mode 100644
index 0000000000..c064e54ba7
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/GPGModule.java
@@ -0,0 +1,37 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+import com.google.inject.AbstractModule;
+import sonia.scm.plugin.Extension;
+import sonia.scm.security.GPG;
+
+@Extension
+public class GPGModule extends AbstractModule {
+ @Override
+ protected void configure() {
+ bind(GPG.class).to(DummyGPG.class);
+ }
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/Keys.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/Keys.java
index 7ecbd1d49d..26f2d94403 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/gpg/Keys.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/Keys.java
@@ -24,18 +24,13 @@
package sonia.scm.security.gpg;
-import org.bouncycastle.bcpg.ArmoredInputStream;
-import org.bouncycastle.gpg.keybox.PublicKeyRingBlob;
import org.bouncycastle.openpgp.PGPException;
-import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPUtil;
-import org.bouncycastle.openpgp.jcajce.JcaPGPPublicKeyRing;
import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator;
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
-import sonia.scm.security.PublicKey;
import java.io.ByteArrayInputStream;
import java.io.IOException;
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java
index 394e657e59..ffb54b355a 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java
@@ -27,20 +27,13 @@ package sonia.scm.security.gpg;
import com.google.common.annotations.VisibleForTesting;
import org.apache.shiro.SecurityUtils;
import org.bouncycastle.openpgp.PGPException;
-import sonia.scm.security.PublicKey;
import sonia.scm.store.DataStore;
import sonia.scm.store.DataStoreFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-import javax.xml.bind.annotation.XmlElement;
-import javax.xml.bind.annotation.XmlRootElement;
import java.io.IOException;
import java.time.Instant;
-import java.util.ArrayList;
-import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
From 4290ca4077839da29995979dcedac18a4a879e71 Mon Sep 17 00:00:00 2001
From: Eduard Heimbuch
Date: Fri, 24 Jul 2020 14:59:28 +0200
Subject: [PATCH 09/46] add config form for public keys
---
.../scm/security/NotPublicKeyException.java | 45 +++++
.../src/main/java/sonia/scm/user/User.java | 2 +-
scm-ui/ui-webapp/public/locales/de/users.json | 11 +-
scm-ui/ui-webapp/public/locales/en/users.json | 11 +-
scm-ui/ui-webapp/src/containers/Profile.tsx | 4 +
.../navLinks/SetPublicKeysNavLink.tsx | 43 ++++
.../src/users/components/navLinks/index.ts | 1 +
.../components/publicKeys/AddPublicKey.tsx | 89 ++++++++
.../components/publicKeys/PublicKeyEntry.tsx | 61 ++++++
.../components/publicKeys/PublicKeyTable.tsx | 62 ++++++
.../components/publicKeys/SetPublicKeys.tsx | 94 +++++++++
.../publicKeys/formatPublicKey.test.ts | 51 +++++
.../components/publicKeys/formatPublicKey.ts | 43 ++++
.../src/users/containers/SingleUser.tsx | 9 +-
.../scm/api/v2/resources/MapperModule.java | 2 +
.../scm/api/v2/resources/MeDtoFactory.java | 3 +
.../scm/api/v2/resources/ResourceLinks.java | 7 +
.../api/v2/resources/UserToUserDtoMapper.java | 1 +
.../gpg/PublicKeyCollectionMapper.java | 86 ++++++++
.../scm/security/gpg/PublicKeyMapper.java | 79 ++++++++
.../scm/security/gpg/PublicKeyResource.java | 191 ++++++++++++++++++
.../scm/security/gpg/PublicKeyStore.java | 43 ++--
.../sonia/scm/security/gpg/RawGpgKey.java | 2 +
.../sonia/scm/security/gpg/RawGpgKeyDto.java | 48 +++++
.../api/v2/resources/MeDtoFactoryTest.java | 22 ++
.../gpg/PublicKeyCollectionMapperTest.java | 110 ++++++++++
.../scm/security/gpg/PublicKeyMapperTest.java | 92 +++++++++
.../security/gpg/PublicKeyResourceTest.java | 142 +++++++++++++
.../scm/security/gpg/PublicKeyStoreTest.java | 85 +++++++-
29 files changed, 1416 insertions(+), 23 deletions(-)
create mode 100644 scm-core/src/main/java/sonia/scm/security/NotPublicKeyException.java
create mode 100644 scm-ui/ui-webapp/src/users/components/navLinks/SetPublicKeysNavLink.tsx
create mode 100644 scm-ui/ui-webapp/src/users/components/publicKeys/AddPublicKey.tsx
create mode 100644 scm-ui/ui-webapp/src/users/components/publicKeys/PublicKeyEntry.tsx
create mode 100644 scm-ui/ui-webapp/src/users/components/publicKeys/PublicKeyTable.tsx
create mode 100644 scm-ui/ui-webapp/src/users/components/publicKeys/SetPublicKeys.tsx
create mode 100644 scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.test.ts
create mode 100644 scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.ts
create mode 100644 scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyCollectionMapper.java
create mode 100644 scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyMapper.java
create mode 100644 scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyResource.java
create mode 100644 scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKeyDto.java
create mode 100644 scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyCollectionMapperTest.java
create mode 100644 scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyMapperTest.java
create mode 100644 scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyResourceTest.java
diff --git a/scm-core/src/main/java/sonia/scm/security/NotPublicKeyException.java b/scm-core/src/main/java/sonia/scm/security/NotPublicKeyException.java
new file mode 100644
index 0000000000..2433b31429
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/security/NotPublicKeyException.java
@@ -0,0 +1,45 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security;
+
+import sonia.scm.BadRequestException;
+import sonia.scm.ContextEntry;
+
+import java.util.List;
+
+public class NotPublicKeyException extends BadRequestException {
+ public NotPublicKeyException(List context, String message) {
+ super(context, message);
+ }
+
+ public NotPublicKeyException(List context, String message, Exception cause) {
+ super(context, message, cause);
+ }
+
+ @Override
+ public String getCode() {
+ return "BxS5wX2v71";
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/user/User.java b/scm-core/src/main/java/sonia/scm/user/User.java
index cd2afb282d..1b0a3b2388 100644
--- a/scm-core/src/main/java/sonia/scm/user/User.java
+++ b/scm-core/src/main/java/sonia/scm/user/User.java
@@ -50,7 +50,7 @@ import java.security.Principal;
@StaticPermissions(
value = "user",
globalPermissions = {"create", "list", "autocomplete"},
- permissions = {"read", "modify", "delete", "changePassword"},
+ permissions = {"read", "modify", "delete", "changePassword", "changePublicKeys"},
custom = true, customGlobal = true
)
@XmlRootElement(name = "users")
diff --git a/scm-ui/ui-webapp/public/locales/de/users.json b/scm-ui/ui-webapp/public/locales/de/users.json
index fd744e768d..8cb79d6d6f 100644
--- a/scm-ui/ui-webapp/public/locales/de/users.json
+++ b/scm-ui/ui-webapp/public/locales/de/users.json
@@ -37,7 +37,8 @@
"settingsNavLink": "Einstellungen",
"generalNavLink": "Generell",
"setPasswordNavLink": "Passwort",
- "setPermissionsNavLink": "Berechtigungen"
+ "setPermissionsNavLink": "Berechtigungen",
+ "setPublicKeyNavLink": "Öffentliche Schlüssel"
}
},
"createUser": {
@@ -60,5 +61,13 @@
"userForm": {
"subtitle": "Benutzer bearbeiten",
"button": "Speichern"
+ },
+ "publicKey": {
+ "noStoredKeys": "Es wurden keine Schlüssel gefunden.",
+ "displayName": "Anzeigename",
+ "raw": "Schlüssel",
+ "created": "Eingetragen an",
+ "addKey": "Schlüssel hinzufügen",
+ "delete": "Löschen"
}
}
diff --git a/scm-ui/ui-webapp/public/locales/en/users.json b/scm-ui/ui-webapp/public/locales/en/users.json
index 3353977a18..0db2b9175f 100644
--- a/scm-ui/ui-webapp/public/locales/en/users.json
+++ b/scm-ui/ui-webapp/public/locales/en/users.json
@@ -37,7 +37,8 @@
"settingsNavLink": "Settings",
"generalNavLink": "General",
"setPasswordNavLink": "Password",
- "setPermissionsNavLink": "Permissions"
+ "setPermissionsNavLink": "Permissions",
+ "setPublicKeyNavLink": "Public Keys"
}
},
"createUser": {
@@ -60,5 +61,13 @@
"userForm": {
"subtitle": "Edit User",
"button": "Submit"
+ },
+ "publicKey": {
+ "noStoredKeys": "No keys found.",
+ "displayName": "Display Name",
+ "raw": "Key",
+ "created": "Created on",
+ "addKey": "Add key",
+ "delete": "Delete"
}
}
diff --git a/scm-ui/ui-webapp/src/containers/Profile.tsx b/scm-ui/ui-webapp/src/containers/Profile.tsx
index 8f14bed1e7..e0700306f8 100644
--- a/scm-ui/ui-webapp/src/containers/Profile.tsx
+++ b/scm-ui/ui-webapp/src/containers/Profile.tsx
@@ -42,6 +42,8 @@ import {
import ChangeUserPassword from "./ChangeUserPassword";
import ProfileInfo from "./ProfileInfo";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
+import SetPublicKeys from "../users/components/publicKeys/SetPublicKeys";
+import SetPublicKeyNavLink from "../users/components/navLinks/SetPublicKeysNavLink";
type Props = RouteComponentProps &
WithTranslation & {
@@ -93,6 +95,7 @@ class Profile extends React.Component {
} />
} />
+ } />
@@ -109,6 +112,7 @@ class Profile extends React.Component {
title={t("profile.settingsNavLink")}
>
+
diff --git a/scm-ui/ui-webapp/src/users/components/navLinks/SetPublicKeysNavLink.tsx b/scm-ui/ui-webapp/src/users/components/navLinks/SetPublicKeysNavLink.tsx
new file mode 100644
index 0000000000..f3144442f5
--- /dev/null
+++ b/scm-ui/ui-webapp/src/users/components/navLinks/SetPublicKeysNavLink.tsx
@@ -0,0 +1,43 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+import React, { FC } from "react";
+import { Link, User, Me } from "@scm-manager/ui-types";
+import { NavLink } from "@scm-manager/ui-components";
+import { useTranslation } from "react-i18next";
+
+type Props = {
+ user: User | Me;
+ publicKeyUrl: string;
+};
+
+const SetPublicKeyNavLink: FC = ({ user, publicKeyUrl }) => {
+ const [t] = useTranslation("users");
+
+ if ((user?._links?.publicKeys as Link)?.href) {
+ return ;
+ }
+ return null;
+};
+
+export default SetPublicKeyNavLink;
diff --git a/scm-ui/ui-webapp/src/users/components/navLinks/index.ts b/scm-ui/ui-webapp/src/users/components/navLinks/index.ts
index 0ccd16b42a..f732ea83ee 100644
--- a/scm-ui/ui-webapp/src/users/components/navLinks/index.ts
+++ b/scm-ui/ui-webapp/src/users/components/navLinks/index.ts
@@ -25,3 +25,4 @@
export { default as EditUserNavLink } from "./EditUserNavLink";
export { default as SetPasswordNavLink } from "./SetPasswordNavLink";
export { default as SetPermissionsNavLink } from "./SetPermissionsNavLink";
+export { default as SetPublicKeysNavLink } from "./SetPublicKeysNavLink";
diff --git a/scm-ui/ui-webapp/src/users/components/publicKeys/AddPublicKey.tsx b/scm-ui/ui-webapp/src/users/components/publicKeys/AddPublicKey.tsx
new file mode 100644
index 0000000000..e0129e8245
--- /dev/null
+++ b/scm-ui/ui-webapp/src/users/components/publicKeys/AddPublicKey.tsx
@@ -0,0 +1,89 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React, { FC, useState } from "react";
+import { User, Link, Links, Collection } from "@scm-manager/ui-types/src";
+import {
+ ErrorNotification,
+ InputField,
+ Level,
+ Textarea,
+ SubmitButton,
+ apiClient,
+ Loading
+} from "@scm-manager/ui-components";
+import { useTranslation } from "react-i18next";
+import { CONTENT_TYPE_PUBLIC_KEY } from "./SetPublicKeys";
+
+type Props = {
+ createLink: string;
+ refresh: () => void;
+};
+
+const AddPublicKey: FC = ({ createLink, refresh }) => {
+ const [t] = useTranslation("users");
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState();
+ const [displayName, setDisplayName] = useState("");
+ const [raw, setRaw] = useState("");
+
+ const isValid = () => {
+ return !!displayName && !!raw;
+ };
+
+ const resetForm = () => {
+ setDisplayName("");
+ setRaw("");
+ };
+
+ const addKey = () => {
+ setLoading(true);
+ apiClient
+ .post(createLink, { displayName: displayName, raw: raw }, CONTENT_TYPE_PUBLIC_KEY)
+ .then(resetForm)
+ .then(refresh)
+ .then(() => setLoading(false))
+ .catch(setError);
+ };
+
+ if (error) {
+ return ;
+ }
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+ <>
+
+
+ }
+ />
+ >
+ );
+};
+
+export default AddPublicKey;
diff --git a/scm-ui/ui-webapp/src/users/components/publicKeys/PublicKeyEntry.tsx b/scm-ui/ui-webapp/src/users/components/publicKeys/PublicKeyEntry.tsx
new file mode 100644
index 0000000000..88c1dfe19e
--- /dev/null
+++ b/scm-ui/ui-webapp/src/users/components/publicKeys/PublicKeyEntry.tsx
@@ -0,0 +1,61 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React, { FC } from "react";
+import { DateFromNow, DeleteButton } from "@scm-manager/ui-components/src";
+import { PublicKey } from "./SetPublicKeys";
+import { useTranslation } from "react-i18next";
+import { Link } from "@scm-manager/ui-types";
+import { formatPublicKey } from "./formatPublicKey";
+
+type Props = {
+ publicKey: PublicKey;
+ onDelete: (link: string) => void;
+};
+
+export const PublicKeyEntry: FC = ({ publicKey, onDelete }) => {
+ const [t] = useTranslation("users");
+
+ let deleteButton;
+ if (publicKey?._links?.delete) {
+ deleteButton = (
+ onDelete((publicKey._links.delete as Link).href)} />
+ );
+ }
+
+ return (
+ <>
+
+
{publicKey.displayName}
+
+
+
+
{formatPublicKey(publicKey.raw)}
+
{deleteButton}
+
+ >
+ );
+};
+
+export default PublicKeyEntry;
diff --git a/scm-ui/ui-webapp/src/users/components/publicKeys/PublicKeyTable.tsx b/scm-ui/ui-webapp/src/users/components/publicKeys/PublicKeyTable.tsx
new file mode 100644
index 0000000000..3172b1429a
--- /dev/null
+++ b/scm-ui/ui-webapp/src/users/components/publicKeys/PublicKeyTable.tsx
@@ -0,0 +1,62 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React, { FC } from "react";
+import { useTranslation } from "react-i18next";
+import { PublicKey, PublicKeysCollection } from "./SetPublicKeys";
+import PublicKeyEntry from "./PublicKeyEntry";
+import { Notification } from "@scm-manager/ui-components";
+
+type Props = {
+ publicKeys?: PublicKeysCollection;
+ onDelete: (link: string) => void;
+};
+
+const PublicKeyTable: FC = ({ publicKeys, onDelete }) => {
+ const [t] = useTranslation("users");
+
+ if (publicKeys?._embedded?.keys?.length === 0) {
+ return {t("publicKey.noStoredKeys")};
+ }
+
+ return (
+
+ );
+};
+
+export default PublicKeyTable;
diff --git a/scm-ui/ui-webapp/src/users/components/publicKeys/SetPublicKeys.tsx b/scm-ui/ui-webapp/src/users/components/publicKeys/SetPublicKeys.tsx
new file mode 100644
index 0000000000..a3c141d35a
--- /dev/null
+++ b/scm-ui/ui-webapp/src/users/components/publicKeys/SetPublicKeys.tsx
@@ -0,0 +1,94 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { Collection, Link, Links, User, Me } from "@scm-manager/ui-types";
+import React, { FC, useEffect, useState } from "react";
+import AddPublicKey from "./AddPublicKey";
+import PublicKeyTable from "./PublicKeyTable";
+import { apiClient, ErrorNotification, Loading } from "@scm-manager/ui-components";
+
+export type PublicKeysCollection = Collection & {
+ _embedded: {
+ keys: PublicKey[];
+ };
+};
+
+export type PublicKey = {
+ displayName: string;
+ raw: string;
+ created?: string;
+ _links: Links;
+};
+
+export const CONTENT_TYPE_PUBLIC_KEY = "application/vnd.scmm-publicKey+json;v=2";
+
+type Props = {
+ user: User | Me;
+};
+
+const SetPublicKeys: FC = ({ user }) => {
+ const [error, setError] = useState();
+ const [loading, setLoading] = useState(false);
+ const [publicKeys, setPublicKeys] = useState(undefined);
+
+ useEffect(() => {
+ fetchPublicKeys();
+ }, [user]);
+
+ const fetchPublicKeys = () => {
+ setLoading(true);
+ apiClient
+ .get((user._links.publicKeys as Link).href)
+ .then(r => r.json())
+ .then(setPublicKeys)
+ .then(() => setLoading(false))
+ .catch(setError);
+ };
+
+ const onDelete = (link: string) => {
+ apiClient
+ .delete(link)
+ .then(fetchPublicKeys)
+ .catch(setError);
+ };
+
+ const createLink = (publicKeys?._links?.create as Link)?.href;
+
+ if (error) {
+ return ;
+ }
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+ <>
+
+ {createLink && }
+ >
+ );
+};
+
+export default SetPublicKeys;
diff --git a/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.test.ts b/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.test.ts
new file mode 100644
index 0000000000..e2377d10b4
--- /dev/null
+++ b/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.test.ts
@@ -0,0 +1,51 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+import { formatPublicKey } from "./formatPublicKey";
+
+describe("format authorized key tests", () => {
+ it("should format the given key", () => {
+ const key = "ssh-rsa ACB0DEFGHIJKLMOPQRSTUVWXYZ tricia@hitchhiker.com";
+ expect(formatPublicKey(key)).toEqual("ssh-rsa ... tricia@hitchhiker.com");
+ });
+
+ it("should use the first chars of the key without prefix", () => {
+ const key = "ACB0DEFGHIJKLMOPQRSTUVWXYZ tricia@hitchhiker.com";
+ expect(formatPublicKey(key)).toEqual("ACB0DEF... tricia@hitchhiker.com");
+ });
+
+ it("should use the last chars of the key without suffix", () => {
+ const key = "ssh-rsa ACB0DEFGHIJKLMOPQRSTUVWXYZ";
+ expect(formatPublicKey(key)).toEqual("ssh-rsa ...TUVWXYZ");
+ });
+
+ it("should use a few chars from the beginning and a few from the end, if the key has no prefix and suffix", () => {
+ const key = "ACB0DEFGHIJKLMOPQRSTUVWXYZ0123456789";
+ expect(formatPublicKey(key)).toEqual("ACB0DEF...3456789");
+ });
+
+ it("should return the whole string for a short key", () => {
+ const key = "ABCDE";
+ expect(formatPublicKey(key)).toEqual("ABCDE");
+ });
+});
diff --git a/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.ts b/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.ts
new file mode 100644
index 0000000000..aa2db2a14c
--- /dev/null
+++ b/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.ts
@@ -0,0 +1,43 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+export const formatPublicKey = (key: string) => {
+ const parts = key.split(/\s+/);
+ if (parts.length === 3) {
+ return parts[0] + " ... " + parts[2];
+ } else if (parts.length === 2) {
+ if (parts[0].length >= parts[1].length) {
+ return parts[0].substring(0, 7) + "... " + parts[1];
+ } else {
+ const keyLength = parts[1].length;
+ return parts[0] + " ..." + parts[1].substring(keyLength - 7);
+ }
+ } else {
+ const keyLength = parts[0].length;
+ if (keyLength < 15) {
+ return parts[0];
+ }
+ return parts[0].substring(0, 7) + "..." + parts[0].substring(keyLength - 7);
+ }
+};
diff --git a/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx b/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx
index ff2c57f0e6..3cc57a642a 100644
--- a/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx
+++ b/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx
@@ -41,11 +41,13 @@ import {
import { Details } from "./../components/table";
import EditUser from "./EditUser";
import { fetchUserByName, getFetchUserFailure, getUserByName, isFetchUserPending } from "../modules/users";
-import { EditUserNavLink, SetPasswordNavLink, SetPermissionsNavLink } from "./../components/navLinks";
+import { EditUserNavLink, SetPasswordNavLink, SetPermissionsNavLink, SetPublicKeysNavLink } from "./../components/navLinks";
import { WithTranslation, withTranslation } from "react-i18next";
import { getUsersLink } from "../../modules/indexResource";
import SetUserPassword from "../components/SetUserPassword";
import SetPermissions from "../../permissions/components/SetPermissions";
+import AddPublicKey from "../components/publicKeys/AddPublicKey";
+import SetPublicKeys from "../components/publicKeys/SetPublicKeys";
type Props = RouteComponentProps &
WithTranslation & {
@@ -105,6 +107,10 @@ class SingleUser extends React.Component {
path={`${url}/settings/permissions`}
component={() => }
/>
+ }
+ />
@@ -123,6 +129,7 @@ class SingleUser extends React.Component {
+
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java
index a45b52ddbe..b6c8e9d8ea 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java
@@ -27,6 +27,7 @@ package sonia.scm.api.v2.resources;
import com.google.inject.AbstractModule;
import com.google.inject.servlet.ServletScopes;
import org.mapstruct.factory.Mappers;
+import sonia.scm.security.gpg.PublicKeyMapper;
import sonia.scm.web.api.RepositoryToHalMapper;
public class MapperModule extends AbstractModule {
@@ -35,6 +36,7 @@ public class MapperModule extends AbstractModule {
bind(UserDtoToUserMapper.class).to(Mappers.getMapperClass(UserDtoToUserMapper.class));
bind(UserToUserDtoMapper.class).to(Mappers.getMapperClass(UserToUserDtoMapper.class));
bind(UserCollectionToDtoMapper.class);
+ bind(PublicKeyMapper.class).to(Mappers.getMapperClass(PublicKeyMapper.class));
bind(GroupDtoToGroupMapper.class).to(Mappers.getMapperClass(GroupDtoToGroupMapper.class));
bind(GroupToGroupDtoMapper.class).to(Mappers.getMapperClass(GroupToGroupDtoMapper.class));
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java
index 5308402540..1dbca4e11a 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java
@@ -89,6 +89,9 @@ public class MeDtoFactory extends HalAppenderMapper {
if (UserPermissions.modify(user).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.me().update(user.getName())));
}
+ if (UserPermissions.changePublicKeys(user).isPermitted()) {
+ linksBuilder.single(link("publicKeys", resourceLinks.user().publicKeys(user.getName())));
+ }
if (userManager.isTypeDefault(user) && UserPermissions.changePassword(user).isPermitted() && !Authentications.isSubjectAnonymous(user.getName())) {
linksBuilder.single(link("password", resourceLinks.me().passwordChange()));
}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java
index 3b76961594..57c962059b 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java
@@ -25,6 +25,7 @@
package sonia.scm.api.v2.resources;
import sonia.scm.repository.NamespaceAndName;
+import sonia.scm.security.gpg.PublicKeyResource;
import javax.inject.Inject;
import java.net.URI;
@@ -99,9 +100,11 @@ class ResourceLinks {
static class UserLinks {
private final LinkBuilder userLinkBuilder;
+ private final LinkBuilder publicKeyLinkBuilder;
UserLinks(ScmPathInfo pathInfo) {
userLinkBuilder = new LinkBuilder(pathInfo, UserRootResource.class, UserResource.class);
+ publicKeyLinkBuilder = new LinkBuilder(pathInfo, PublicKeyResource.class);
}
String self(String name) {
@@ -119,6 +122,10 @@ class ResourceLinks {
public String passwordChange(String name) {
return userLinkBuilder.method("getUserResource").parameters(name).method("overwritePassword").parameters().href();
}
+
+ public String publicKeys(String name) {
+ return publicKeyLinkBuilder.method("findAll").parameters(name).href();
+ }
}
interface WithPermissionLinks {
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java
index 322c951962..761de187f1 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java
@@ -65,6 +65,7 @@ public abstract class UserToUserDtoMapper extends BaseMapper {
}
if (UserPermissions.modify(user).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.user().update(user.getName())));
+ linksBuilder.single(link("publicKeys", resourceLinks.user().publicKeys(user.getName())));
if (userManager.isTypeDefault(user)) {
linksBuilder.single(link("password", resourceLinks.user().passwordChange(user.getName())));
}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyCollectionMapper.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyCollectionMapper.java
new file mode 100644
index 0000000000..eeffff49c9
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyCollectionMapper.java
@@ -0,0 +1,86 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+import de.otto.edison.hal.Embedded;
+import de.otto.edison.hal.HalRepresentation;
+import de.otto.edison.hal.Link;
+import de.otto.edison.hal.Links;
+import sonia.scm.api.v2.resources.LinkBuilder;
+import sonia.scm.api.v2.resources.ScmPathInfoStore;
+import sonia.scm.user.UserPermissions;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static de.otto.edison.hal.Links.linkingTo;
+
+public class PublicKeyCollectionMapper {
+
+ private final Provider scmPathInfoStore;
+ private final PublicKeyMapper mapper;
+
+ @Inject
+ public PublicKeyCollectionMapper(Provider scmPathInfoStore, PublicKeyMapper mapper) {
+ this.scmPathInfoStore = scmPathInfoStore;
+ this.mapper = mapper;
+ }
+
+ HalRepresentation map(String username, List keys) {
+ List dtos = keys.stream()
+ .map(mapper::map)
+ .collect(Collectors.toList());
+
+ Links.Builder builder = linkingTo();
+
+ builder.self(selfLink(username));
+
+ if (hasCreatePermissions(username)) {
+ builder.single(Link.link("create", createLink(username)));
+ }
+
+ return new HalRepresentation(builder.build(), Embedded.embedded("keys", dtos));
+ }
+
+ private boolean hasCreatePermissions(String username) {
+ return UserPermissions.changePublicKeys(username).isPermitted();
+ }
+
+ private String createLink(String username) {
+ return new LinkBuilder(scmPathInfoStore.get().get(), PublicKeyResource.class)
+ .method("create")
+ .parameters(username)
+ .href();
+ }
+
+ private String selfLink(String username) {
+ return new LinkBuilder(scmPathInfoStore.get().get(), PublicKeyResource.class)
+ .method("findAll")
+ .parameters(username)
+ .href();
+ }
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyMapper.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyMapper.java
new file mode 100644
index 0000000000..fbd6a5cff9
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyMapper.java
@@ -0,0 +1,79 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+import com.google.common.annotations.VisibleForTesting;
+import de.otto.edison.hal.Link;
+import de.otto.edison.hal.Links;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.ObjectFactory;
+import sonia.scm.api.v2.resources.LinkBuilder;
+import sonia.scm.api.v2.resources.ScmPathInfoStore;
+import sonia.scm.user.UserPermissions;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+import static de.otto.edison.hal.Links.linkingTo;
+
+@Mapper
+public abstract class PublicKeyMapper {
+
+ @Inject
+ private Provider scmPathInfoStore;
+
+ @VisibleForTesting
+ void setScmPathInfoStore(Provider scmPathInfoStore) {
+ this.scmPathInfoStore = scmPathInfoStore;
+ }
+
+ @Mapping(target = "attributes", ignore = true)
+ abstract RawGpgKeyDto map(RawGpgKey rawGpgKey);
+
+ @ObjectFactory
+ RawGpgKeyDto createDto(RawGpgKey rawGpgKey) {
+ Links.Builder linksBuilder = linkingTo();
+ linksBuilder.self(createSelfLink(rawGpgKey));
+ if (UserPermissions.changePublicKeys(rawGpgKey.getOwner()).isPermitted()) {
+ linksBuilder.single(Link.link("delete", createDeleteLink(rawGpgKey)));
+ }
+ return new RawGpgKeyDto(linksBuilder.build());
+ }
+
+ private String createSelfLink(RawGpgKey rawGpgKey) {
+ return new LinkBuilder(scmPathInfoStore.get().get(), PublicKeyResource.class)
+ .method("findById")
+ .parameters(rawGpgKey.getId())
+ .href();
+ }
+
+ private String createDeleteLink(RawGpgKey rawGpgKey) {
+ return new LinkBuilder(scmPathInfoStore.get().get(), PublicKeyResource.class)
+ .method("deleteById")
+ .parameters(rawGpgKey.getId())
+ .href();
+ }
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyResource.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyResource.java
new file mode 100644
index 0000000000..de927f134b
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyResource.java
@@ -0,0 +1,191 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+import de.otto.edison.hal.HalRepresentation;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import sonia.scm.api.v2.resources.ErrorDto;
+import sonia.scm.web.VndMediaType;
+
+import javax.inject.Inject;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import java.util.Optional;
+
+@Path("v2/public_keys")
+public class PublicKeyResource {
+
+ private static final String MEDIA_TYPE = VndMediaType.PREFIX + "publicKey" + VndMediaType.SUFFIX;
+ private static final String MEDIA_TYPE_COLLECTION = VndMediaType.PREFIX + "publicKeyCollection" + VndMediaType.SUFFIX;
+
+ private final PublicKeyMapper mapper;
+ private final PublicKeyCollectionMapper collectionMapper;
+ private final PublicKeyStore store;
+
+ @Inject
+ public PublicKeyResource(PublicKeyMapper mapper, PublicKeyCollectionMapper collectionMapper, PublicKeyStore store) {
+ this.mapper = mapper;
+ this.collectionMapper = collectionMapper;
+ this.store = store;
+ }
+
+ @GET
+ @Path("{username}")
+ @Produces(MEDIA_TYPE_COLLECTION)
+ @Operation(
+ summary = "Get all public keys for user",
+ description = "Returns all keys for the given username.",
+ tags = "User",
+ operationId = "get_all_public_keys"
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "success",
+ content = @Content(
+ mediaType = MEDIA_TYPE_COLLECTION,
+ schema = @Schema(implementation = HalRepresentation.class)
+ )
+ )
+ @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
+ @ApiResponse(responseCode = "403", description = "not authorized / the current user does not have the right privilege")
+ @ApiResponse(
+ responseCode = "500",
+ description = "internal server error",
+ content = @Content(
+ mediaType = VndMediaType.ERROR_TYPE,
+ schema = @Schema(implementation = ErrorDto.class)
+ )
+ )
+ public HalRepresentation findAll(@PathParam("username") String username) {
+ return collectionMapper.map(username, store.findByUsername(username));
+ }
+
+ @GET
+ @Path("{id}")
+ @Produces(MEDIA_TYPE)
+ @Operation(
+ summary = "Get single key for user",
+ description = "Returns a single public key for username by id.",
+ tags = "User",
+ operationId = "get_single_public_key"
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "success",
+ content = @Content(
+ mediaType = MEDIA_TYPE,
+ schema = @Schema(implementation = RawGpgKeyDto.class)
+ )
+ )
+ @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
+ @ApiResponse(responseCode = "403", description = "not authorized / the current user does not have the right privilege")
+ @ApiResponse(
+ responseCode = "404",
+ description = "not found / key for given id not available",
+ content = @Content(
+ mediaType = VndMediaType.ERROR_TYPE,
+ schema = @Schema(implementation = ErrorDto.class)
+ )
+ )
+ @ApiResponse(
+ responseCode = "500",
+ description = "internal server error",
+ content = @Content(
+ mediaType = VndMediaType.ERROR_TYPE,
+ schema = @Schema(implementation = ErrorDto.class)
+ )
+ )
+ public Response findById(@PathParam("id") String id) {
+ Optional byId = store.findById(id);
+ if (byId.isPresent()) {
+ return Response.ok(mapper.map(byId.get())).build();
+ }
+ return Response.status(Response.Status.NOT_FOUND).build();
+ }
+
+ @POST
+ @Path("{username}")
+ @Consumes(MEDIA_TYPE)
+ @Operation(
+ summary = "Create new key",
+ description = "Creates new key for user.",
+ tags = "User",
+ operationId = "create_public_key"
+ )
+ @ApiResponse(responseCode = "201", description = "create success")
+ @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
+ @ApiResponse(responseCode = "403", description = "not authorized / the current user does not have the right privilege")
+ @ApiResponse(
+ responseCode = "500",
+ description = "internal server error",
+ content = @Content(
+ mediaType = VndMediaType.ERROR_TYPE,
+ schema = @Schema(implementation = ErrorDto.class)
+ )
+ )
+ public Response create(@Context UriInfo uriInfo, @PathParam("username") String username, RawGpgKeyDto publicKey) {
+ String id = store.add(publicKey.getDisplayName(), username, publicKey.getRaw()).getId();
+ UriBuilder builder = uriInfo.getAbsolutePathBuilder();
+ builder.path(id);
+ return Response.created(builder.build()).build();
+ }
+
+ @DELETE
+ @Path("delete/{id}")
+ @Operation(
+ summary = "Deletes public key",
+ description = "Deletes public key for user.",
+ tags = "User",
+ operationId = "delete_public_key"
+ )
+ @ApiResponse(responseCode = "204", description = "delete success")
+ @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
+ @ApiResponse(responseCode = "403", description = "not authorized / the current user does not have the right privilege")
+ @ApiResponse(
+ responseCode = "500",
+ description = "internal server error",
+ content = @Content(
+ mediaType = VndMediaType.ERROR_TYPE,
+ schema = @Schema(implementation = ErrorDto.class)
+ )
+ )
+ public Response deleteById(@PathParam("id") String id) {
+ store.delete(id);
+ return Response.noContent().build();
+ }
+
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java
index ffb54b355a..cbfa1b5302 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java
@@ -24,18 +24,20 @@
package sonia.scm.security.gpg;
-import com.google.common.annotations.VisibleForTesting;
-import org.apache.shiro.SecurityUtils;
import org.bouncycastle.openpgp.PGPException;
+import sonia.scm.ContextEntry;
+import sonia.scm.security.NotPublicKeyException;
import sonia.scm.store.DataStore;
import sonia.scm.store.DataStoreFactory;
+import sonia.scm.user.UserPermissions;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.time.Instant;
+import java.util.List;
import java.util.Optional;
-import java.util.function.Supplier;
+import java.util.stream.Collectors;
@Singleton
public class PublicKeyStore {
@@ -43,26 +45,22 @@ public class PublicKeyStore {
private static final String STORE_NAME = "gpg_public_keys";
private final DataStore store;
- private final Supplier currentUserSupplier;
@Inject
public PublicKeyStore(DataStoreFactory dataStoreFactory) {
- this(
- dataStoreFactory.withType(RawGpgKey.class).withName(STORE_NAME).build(),
- () -> SecurityUtils.getSubject().getPrincipal().toString()
- );
+ this.store = dataStoreFactory.withType(RawGpgKey.class).withName(STORE_NAME).build();
}
- @VisibleForTesting
- PublicKeyStore(DataStore store, Supplier currentUserSupplier) {
- this.store = store;
- this.currentUserSupplier = currentUserSupplier;
- }
+ public RawGpgKey add(String displayName, String username, String rawKey) {
+ UserPermissions.modify(username).check();
+
+ if (!rawKey.contains("PUBLIC KEY")) {
+ throw new NotPublicKeyException(ContextEntry.ContextBuilder.entity(RawGpgKey.class, displayName).build(), "The provided key is not a public key");
+ }
- public RawGpgKey add(String displayName, String rawKey) {
try {
String id = Keys.resolveIdFromKey(rawKey);
- RawGpgKey key = new RawGpgKey(id, displayName, currentUserSupplier.get(), rawKey, Instant.now());
+ RawGpgKey key = new RawGpgKey(id, displayName, username, rawKey, Instant.now());
store.put(id, key);
@@ -72,8 +70,23 @@ public class PublicKeyStore {
}
}
+ public void delete(String id) {
+ RawGpgKey rawGpgKey = store.get(id);
+ if (rawGpgKey != null) {
+ UserPermissions.modify(rawGpgKey.getOwner()).check();
+ store.remove(id);
+ }
+ }
+
public Optional findById(String id) {
return store.getOptional(id);
}
+ public List findByUsername(String username) {
+ return store.getAll().values()
+ .stream()
+ .filter(rawGpgKey -> username.equalsIgnoreCase(rawGpgKey.getOwner()))
+ .collect(Collectors.toList());
+ }
+
}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKey.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKey.java
index 5b098c5af3..f32d757d24 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKey.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKey.java
@@ -32,6 +32,7 @@ import sonia.scm.xml.XmlInstantAdapter;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.time.Instant;
import java.util.Objects;
@@ -40,6 +41,7 @@ import java.util.Objects;
@NoArgsConstructor
@AllArgsConstructor
@XmlAccessorType(XmlAccessType.FIELD)
+@XmlRootElement
public class RawGpgKey {
private String id;
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKeyDto.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKeyDto.java
new file mode 100644
index 0000000000..2e40555e42
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKeyDto.java
@@ -0,0 +1,48 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+import de.otto.edison.hal.HalRepresentation;
+import de.otto.edison.hal.Links;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.Instant;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@SuppressWarnings("squid:S2160") // we do not need equals for dto
+public class RawGpgKeyDto extends HalRepresentation {
+
+ private String displayName;
+ private String raw;
+ private Instant created;
+
+ RawGpgKeyDto(Links links) {
+ super(links);
+ }
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java
index cffd6dc840..d82e153355 100644
--- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java
@@ -198,6 +198,28 @@ class MeDtoFactoryTest {
assertThat(dto.getLinks().getLinkBy("password")).isNotPresent();
}
+ @Test
+ void shouldAppendPublicKeysLink() {
+ User user = UserTestData.createTrillian();
+ prepareSubject(user);
+
+ when(subject.isPermitted("user:changePublicKeys:trillian")).thenReturn(true);
+
+ MeDto dto = meDtoFactory.create();
+ assertThat(dto.getLinks().getLinkBy("publicKeys").get().getHref()).isEqualTo("https://scm.hitchhiker.com/scm/v2/public_keys/trillian");
+ }
+
+ @Test
+ void shouldNotAppendPublicKeysLink() {
+ User user = UserTestData.createTrillian();
+ prepareSubject(user);
+
+ when(subject.isPermitted("user:changePublicKeys:trillian")).thenReturn(false);
+
+ MeDto dto = meDtoFactory.create();
+ assertThat(dto.getLinks().getLinkBy("publicKeys")).isNotPresent();
+ }
+
@Test
void shouldAppendLinks() {
prepareSubject(UserTestData.createTrillian());
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyCollectionMapperTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyCollectionMapperTest.java
new file mode 100644
index 0000000000..6bd69efdc1
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyCollectionMapperTest.java
@@ -0,0 +1,110 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+import com.google.common.collect.Lists;
+import com.google.inject.util.Providers;
+import de.otto.edison.hal.HalRepresentation;
+import org.apache.shiro.subject.Subject;
+import org.apache.shiro.util.ThreadContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import sonia.scm.api.v2.resources.ScmPathInfoStore;
+
+import java.io.IOException;
+import java.net.URI;
+import java.time.Instant;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class PublicKeyCollectionMapperTest {
+
+
+ private PublicKeyCollectionMapper collectionMapper;
+
+ @Mock
+ private PublicKeyMapper mapper;
+
+ @Mock
+ private Subject subject;
+
+ @BeforeEach
+ void setUpObjectUnderTest() {
+ ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
+ pathInfoStore.set(() -> URI.create("/"));
+ collectionMapper = new PublicKeyCollectionMapper(Providers.of(pathInfoStore), mapper);
+
+ ThreadContext.bind(subject);
+ }
+
+ @AfterEach
+ void cleanThreadContext() {
+ ThreadContext.unbindSubject();
+ }
+
+ @Test
+ void shouldMapToCollection() throws IOException {
+ when(mapper.map(any(RawGpgKey.class))).then(ic -> new RawGpgKeyDto());
+
+ RawGpgKey one = createPublicKey("one");
+ RawGpgKey two = createPublicKey("two");
+
+ List keys = Lists.newArrayList(one, two);
+ HalRepresentation collection = collectionMapper.map("trillian", keys);
+
+ List embedded = collection.getEmbedded().getItemsBy("keys");
+ assertThat(embedded).hasSize(2);
+
+ assertThat(collection.getLinks().getLinkBy("self").get().getHref()).isEqualTo("/v2/public_keys/trillian");
+ }
+
+ @Test
+ void shouldAddCreateLinkIfTheUserIsPermitted() {
+ when(subject.isPermitted("user:changePublicKeys:trillian")).thenReturn(true);
+
+ HalRepresentation collection = collectionMapper.map("trillian", Lists.newArrayList());
+ assertThat(collection.getLinks().getLinkBy("create").get().getHref()).isEqualTo("/v2/public_keys/trillian");
+ }
+
+ @Test
+ void shouldNotAddCreateLinkWithoutPermission() {
+ HalRepresentation collection = collectionMapper.map("trillian", Lists.newArrayList());
+ assertThat(collection.getLinks().getLinkBy("create")).isNotPresent();
+ }
+
+ private RawGpgKey createPublicKey(String displayName) throws IOException {
+ String raw = GPGTestHelper.readKey("single.asc");
+ return new RawGpgKey(displayName, displayName, "trillian", raw, Instant.now());
+ }
+
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyMapperTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyMapperTest.java
new file mode 100644
index 0000000000..0e7bd496c8
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyMapperTest.java
@@ -0,0 +1,92 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+import com.google.inject.util.Providers;
+import org.apache.shiro.subject.Subject;
+import org.apache.shiro.util.ThreadContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import sonia.scm.api.v2.resources.ScmPathInfoStore;
+
+import java.io.IOException;
+import java.net.URI;
+import java.time.Instant;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class PublicKeyMapperTest {
+
+ @Mock
+ private Subject subject;
+
+ private final PublicKeyMapper mapper = new PublicKeyMapperImpl();
+ ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
+
+ @BeforeEach
+ void setup() {
+ ThreadContext.bind(subject);
+
+ pathInfoStore.set(() -> URI.create("/"));
+ mapper.setScmPathInfoStore(Providers.of(pathInfoStore));
+ }
+
+ @AfterEach
+ void tearDownSubject() {
+ ThreadContext.unbindSubject();
+ }
+
+ @Test
+ void shouldMapKeyToDto() throws IOException {
+ when(subject.isPermitted("user:changePublicKeys:trillian")).thenReturn(true);
+
+ String raw = GPGTestHelper.readKey("single.asc");
+ RawGpgKey key = new RawGpgKey("1", "key_42", "trillian", raw, Instant.now());
+
+ RawGpgKeyDto dto = mapper.map(key);
+
+ assertThat(dto.getDisplayName()).isEqualTo(key.getDisplayName());
+ assertThat(dto.getRaw()).isEqualTo(key.getRaw());
+ assertThat(dto.getCreated()).isEqualTo(key.getCreated());
+ assertThat(dto.getLinks().getLinkBy("self").get().getHref()).isEqualTo("/v2/public_keys/1");
+ assertThat(dto.getLinks().getLinkBy("delete").get().getHref()).isEqualTo("/v2/public_keys/delete/1");
+ }
+
+ @Test
+ void shouldNotAppendDeleteLink() throws IOException {
+ String raw = GPGTestHelper.readKey("single.asc");
+ RawGpgKey key = new RawGpgKey("1", "key_42", "trillian", raw, Instant.now());
+
+ RawGpgKeyDto dto = mapper.map(key);
+
+ assertThat(dto.getLinks().getLinkBy("delete")).isNotPresent();
+ }
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyResourceTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyResourceTest.java
new file mode 100644
index 0000000000..112fc1b31f
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyResourceTest.java
@@ -0,0 +1,142 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+import de.otto.edison.hal.HalRepresentation;
+import org.apache.shiro.subject.Subject;
+import org.apache.shiro.util.ThreadContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class PublicKeyResourceTest {
+
+ @Mock
+ private PublicKeyStore store;
+
+ @Mock
+ private PublicKeyCollectionMapper collectionMapper;
+
+ @Mock
+ private PublicKeyMapper mapper;
+
+ @InjectMocks
+ private PublicKeyResource resource;
+
+ @Mock
+ private Subject subject;
+
+ @BeforeEach
+ void setUpSubject() {
+ ThreadContext.bind(subject);
+ }
+
+ @AfterEach
+ void clearSubject() {
+ ThreadContext.unbindSubject();
+ }
+
+ @Test
+ void shouldFindAll() {
+ List keys = new ArrayList<>();
+ when(store.findByUsername("trillian")).thenReturn(keys);
+
+ HalRepresentation collection = new HalRepresentation();
+ when(collectionMapper.map("trillian", keys)).thenReturn(collection);
+
+ HalRepresentation result = resource.findAll("trillian");
+ assertThat(result).isSameAs(collection);
+ }
+
+ @Test
+ void shouldFindById() {
+ RawGpgKey key = new RawGpgKey("42");
+ when(store.findById("42")).thenReturn(Optional.of(key));
+ RawGpgKeyDto dto = new RawGpgKeyDto();
+ when(mapper.map(key)).thenReturn(dto);
+
+ Response response = resource.findById("42");
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThat(response.getEntity()).isSameAs(dto);
+ }
+
+ @Test
+ void shouldReturn404IfIdDoesNotExists() {
+ when(store.findById("42")).thenReturn(Optional.empty());
+
+ Response response = resource.findById("42");
+ assertThat(response.getStatus()).isEqualTo(404);
+ }
+
+ @Test
+ void shouldAddToStore() throws URISyntaxException, IOException {
+ String raw = GPGTestHelper.readKey("single.asc");
+
+ UriInfo uriInfo = mock(UriInfo.class);
+ UriBuilder builder = mock(UriBuilder.class);
+ when(uriInfo.getAbsolutePathBuilder()).thenReturn(builder);
+ when(builder.path("42")).thenReturn(builder);
+ when(builder.build()).thenReturn(new URI("/v2/public_keys/42"));
+
+ RawGpgKey key = new RawGpgKey("42");
+ RawGpgKeyDto dto = new RawGpgKeyDto();
+ dto.setDisplayName("key_42");
+ dto.setRaw(raw);
+ when(store.add(dto.getDisplayName(), "trillian", dto.getRaw())).thenReturn(key);
+
+ Response response = resource.create(uriInfo, "trillian", dto);
+
+ assertThat(response.getStatus()).isEqualTo(201);
+ assertThat(response.getLocation().toASCIIString()).isEqualTo("/v2/public_keys/42");
+ }
+
+ @Test
+ void shouldDeleteFromStore() {
+ Response response = resource.deleteById("42");
+ assertThat(response.getStatus()).isEqualTo(204);
+ verify(store).delete("42");
+ }
+
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java
index 3c26256247..1b4e20cffc 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java
@@ -24,23 +24,65 @@
package sonia.scm.security.gpg;
+import org.apache.shiro.authz.AuthorizationException;
+import org.apache.shiro.subject.Subject;
+import org.apache.shiro.util.ThreadContext;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import sonia.scm.store.InMemoryDataStore;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import sonia.scm.security.NotPublicKeyException;
+import sonia.scm.store.DataStoreFactory;
+import sonia.scm.store.InMemoryDataStoreFactory;
import java.io.IOException;
import java.time.Instant;
+import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.doThrow;
+@ExtendWith(MockitoExtension.class)
class PublicKeyStoreTest {
+ @Mock
+ private Subject subject;
+
private PublicKeyStore keyStore;
+ private final DataStoreFactory dataStoreFactory = new InMemoryDataStoreFactory();
@BeforeEach
void setUpKeyStore() {
- keyStore = new PublicKeyStore(new InMemoryDataStore<>(), () -> "trillian");
+ keyStore = new PublicKeyStore(dataStoreFactory);
+ }
+
+ @BeforeEach
+ void bindSubject() {
+ ThreadContext.bind(subject);
+ }
+
+ @AfterEach
+ void tearDownSubject() {
+ ThreadContext.unbindSubject();
+ }
+
+ @Test
+ void shouldThrowAuthorizationExceptionOnAdd() throws IOException {
+ doThrow(AuthorizationException.class).when(subject).checkPermission("user:modify:zaphod");
+ String rawKey = GPGTestHelper.readKey("single.asc");
+
+ assertThrows(AuthorizationException.class, () -> keyStore.add("zaphods key", "zaphod", rawKey));
+ }
+
+ @Test
+ void shouldOnlyStorePublicKeys() throws IOException {
+ String rawKey = GPGTestHelper.readKey("single.asc").replace("PUBLIC", "PRIVATE");
+
+ assertThrows(NotPublicKeyException.class, () -> keyStore.add("SCM Package Key", "trillian", rawKey));
}
@Test
@@ -48,7 +90,7 @@ class PublicKeyStoreTest {
String rawKey = GPGTestHelper.readKey("single.asc");
Instant now = Instant.now();
- RawGpgKey key = keyStore.add("SCM Package Key", rawKey);
+ RawGpgKey key = keyStore.add("SCM Package Key", "trillian", rawKey);
assertThat(key.getId()).isEqualTo("0x975922F193B07D6E");
assertThat(key.getDisplayName()).isEqualTo("SCM Package Key");
assertThat(key.getOwner()).isEqualTo("trillian");
@@ -59,9 +101,44 @@ class PublicKeyStoreTest {
@Test
void shouldFindStoredKeyById() throws IOException {
String rawKey = GPGTestHelper.readKey("single.asc");
- keyStore.add("SCM Package Key", rawKey);
+ keyStore.add("SCM Package Key", "trillian", rawKey);
Optional key = keyStore.findById("0x975922F193B07D6E");
assertThat(key).isPresent();
}
+ @Test
+ void shouldDeleteKey() throws IOException {
+ String rawKey = GPGTestHelper.readKey("single.asc");
+ keyStore.add("SCM Package Key", "trillian", rawKey);
+ Optional key = keyStore.findById("0x975922F193B07D6E");
+
+ assertThat(key).isPresent();
+
+ keyStore.delete("0x975922F193B07D6E");
+ key = keyStore.findById("0x975922F193B07D6E");
+
+ assertThat(key).isNotPresent();
+ }
+
+ @Test
+ void shouldReturnEmptyListIfNoKeysAvailable() {
+ List keys = keyStore.findByUsername("zaphod");
+
+ assertThat(keys).isEmpty();
+ assertThat(keys).isInstanceOf(List.class);
+ }
+
+ @Test
+ void shouldFindAllKeysForUser() throws IOException {
+ String singleKey = GPGTestHelper.readKey("single.asc");
+ keyStore.add("SCM Single Key", "trillian", singleKey);
+
+ String multiKey = GPGTestHelper.readKey("subkeys.asc");
+ keyStore.add("SCM Multi Key", "trillian", multiKey);
+
+ List keys = keyStore.findByUsername("trillian");
+
+ assertThat(keys.size()).isEqualTo(2);
+ }
+
}
From 0c45cf21e3e1745d3eedb75cb30b31889df2fb63 Mon Sep 17 00:00:00 2001
From: Eduard Heimbuch
Date: Tue, 28 Jul 2020 12:45:20 +0200
Subject: [PATCH 10/46] implement default gpg using bouncy castle
---
.../src/main/java/sonia/scm/security/GPG.java | 3 +
.../gpg/{DummyGPG.java => DefaultGPG.java} | 39 +++++--
.../sonia/scm/security/gpg/GPGModule.java | 2 +-
.../java/sonia/scm/security/gpg/GpgKey.java | 102 ++++++++++++++++++
.../scm/security/gpg/DefaultGPGTest.java | 86 +++++++++++++++
.../sonia/scm/security/gpg/GpgKeyTest.java | 51 +++++++++
6 files changed, 276 insertions(+), 7 deletions(-)
rename scm-webapp/src/main/java/sonia/scm/security/gpg/{DummyGPG.java => DefaultGPG.java} (63%)
create mode 100644 scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java
create mode 100644 scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultGPGTest.java
create mode 100644 scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java
diff --git a/scm-core/src/main/java/sonia/scm/security/GPG.java b/scm-core/src/main/java/sonia/scm/security/GPG.java
index 2fa773c906..2f75b8d5dc 100644
--- a/scm-core/src/main/java/sonia/scm/security/GPG.java
+++ b/scm-core/src/main/java/sonia/scm/security/GPG.java
@@ -35,6 +35,7 @@ public interface GPG {
/**
* Returns the id of the key from the given signature.
+ *
* @param signature signature
* @return public key id
*/
@@ -42,6 +43,7 @@ public interface GPG {
/**
* Returns the public key with the given id or an empty optional.
+ *
* @param id id of public
* @return public key or empty optional
*/
@@ -49,6 +51,7 @@ public interface GPG {
/**
* Returns all public keys assigned to the given username
+ *
* @param username username of the public key owner
* @return collection of public keys
*/
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/DummyGPG.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/DefaultGPG.java
similarity index 63%
rename from scm-webapp/src/main/java/sonia/scm/security/gpg/DummyGPG.java
rename to scm-webapp/src/main/java/sonia/scm/security/gpg/DefaultGPG.java
index 79c6e516bb..792610e30c 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/gpg/DummyGPG.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/DefaultGPG.java
@@ -24,30 +24,57 @@
package sonia.scm.security.gpg;
+import org.bouncycastle.openpgp.PGPException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import sonia.scm.security.GPG;
import sonia.scm.security.PrivateKey;
import sonia.scm.security.PublicKey;
+import javax.inject.Inject;
+import java.io.IOException;
import java.util.Collections;
+import java.util.List;
import java.util.Optional;
+import java.util.stream.Collectors;
-/**
- * Dummy implementation of {@link GPG} should be replaced soon.
- */
-public class DummyGPG implements GPG {
+public class DefaultGPG implements GPG {
+
+ private static final Logger LOG = LoggerFactory.getLogger(DefaultGPG.class);
+ private final PublicKeyStore store;
+
+ @Inject
+ public DefaultGPG(PublicKeyStore store) {
+ this.store = store;
+ }
@Override
public String findPublicKeyId(byte[] signature) {
- return "unknown";
+ try {
+ return Keys.resolveIdFromKey(new String(signature));
+ } catch (PGPException | IOException e) {
+ LOG.error("Could not find public key id in signature");
+ }
+ return "";
}
@Override
public Optional findPublicKey(String id) {
- return Optional.empty();
+ Optional key = store.findById(id);
+ return key.map(rawGpgKey -> new GpgKey(id, rawGpgKey.getOwner()));
}
@Override
public Iterable findPublicKeysByUsername(String username) {
+ List keys = store.findByUsername(username);
+
+ if (!keys.isEmpty()) {
+ return keys
+ .stream()
+ .map(rawGpgKey -> new GpgKey(rawGpgKey.getId(), rawGpgKey.getOwner()))
+ .collect(Collectors.toSet());
+ }
+
return Collections.emptySet();
}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/GPGModule.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/GPGModule.java
index c064e54ba7..7087a971a4 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/gpg/GPGModule.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/GPGModule.java
@@ -32,6 +32,6 @@ import sonia.scm.security.GPG;
public class GPGModule extends AbstractModule {
@Override
protected void configure() {
- bind(GPG.class).to(DummyGPG.class);
+ bind(GPG.class).to(DefaultGPG.class);
}
}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java
new file mode 100644
index 0000000000..26a903744b
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java
@@ -0,0 +1,102 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPObjectFactory;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider;
+import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
+import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sonia.scm.security.PublicKey;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+
+public class GpgKey implements PublicKey {
+
+ private static final Logger LOG = LoggerFactory.getLogger(GpgKey.class);
+
+ private final String id;
+ private final String owner;
+
+ public GpgKey(String id, String owner) {
+ this.id = id;
+ this.owner = owner;
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public Optional getOwner() {
+ if (owner == null) {
+ return Optional.empty();
+ }
+ return Optional.of(owner);
+ }
+
+ @Override
+ public boolean verify(InputStream stream, byte[] signature) {
+ boolean verified = false;
+ try {
+ ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(signature));
+ PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(armoredInputStream, new JcaKeyFingerprintCalculator());
+ PGPPublicKey publicKey = ((PGPPublicKeyRing) pgpObjectFactory.nextObject()).getPublicKey();
+ PGPSignature pgpSignature = ((PGPSignature) publicKey.getSignatures().next());
+
+ PGPContentVerifierBuilderProvider provider = new JcaPGPContentVerifierBuilderProvider();
+ pgpSignature.init(provider, publicKey);
+
+ char[] buffer = new char[1024];
+ int bytesRead = 0;
+ BufferedReader in = new BufferedReader(new InputStreamReader(stream));
+
+ while (bytesRead != -1) {
+ bytesRead = in.read(buffer, 0, 1024);
+ pgpSignature.update(new String(buffer).getBytes(StandardCharsets.UTF_8));
+ }
+
+ verified = pgpSignature.verify();
+ } catch (IOException | PGPException e) {
+ LOG.error("Could not verify GPG key", e);
+ }
+ return verified;
+ }
+}
+
+
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultGPGTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultGPGTest.java
new file mode 100644
index 0000000000..63f39bd523
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultGPGTest.java
@@ -0,0 +1,86 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import sonia.scm.security.PublicKey;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class DefaultGPGTest {
+
+ @Mock
+ private PublicKeyStore store;
+
+ @InjectMocks
+ private DefaultGPG gpg;
+
+ @Test
+ void shouldFindIdInSignature() throws IOException {
+ String raw = GPGTestHelper.readKey("single.asc");
+ String publicKeyId = gpg.findPublicKeyId(raw.getBytes());
+
+ assertThat(publicKeyId).isEqualTo("0x975922F193B07D6E");
+ }
+
+ @Test
+ void shouldFindPublicKey() {
+ RawGpgKey key1 = new RawGpgKey("42", "key_42", "trillian", "raw", Instant.now());
+
+ when(store.findById("42")).thenReturn(Optional.of(key1));
+
+ Optional publicKey = gpg.findPublicKey("42");
+
+ assertThat(publicKey).isPresent();
+ assertThat(publicKey.get().getOwner()).isPresent();
+ assertThat(publicKey.get().getOwner().get()).contains("trillian");
+ assertThat(publicKey.get().getId()).isEqualTo("42");
+ }
+
+ @Test
+ void shouldFindKeysForUsername() {
+ RawGpgKey key1 = new RawGpgKey("1", "1", "trillian", "raw", Instant.now());
+ RawGpgKey key2 = new RawGpgKey("2", "2", "trillian", "raw", Instant.now());
+ when(store.findByUsername("trillian")).thenReturn(ImmutableList.of(key1, key2));
+
+ Iterable keys = gpg.findPublicKeysByUsername("trillian");
+
+ assertThat(keys).hasSize(2);
+ PublicKey key = keys.iterator().next();
+ assertThat(key.getOwner()).isPresent();
+ assertThat(key.getOwner().get()).contains("trillian");
+ }
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java
new file mode 100644
index 0000000000..86bf5de4e0
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java
@@ -0,0 +1,51 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class GpgKeyTest {
+
+ @Test
+ void shouldVerifyPublicKey() throws IOException {
+ StringBuilder longContent = new StringBuilder();
+ for (int i = 1; i < 10000; i++) {
+ longContent.append(i);
+ }
+
+ byte[] raw = GPGTestHelper.readKey("subkeys.asc").getBytes();
+
+ GpgKey key = new GpgKey("1", "trillian");
+
+ boolean verified = key.verify(longContent.toString().getBytes(), raw);
+
+ // assertThat(verified).isTrue();
+ }
+
+}
From b22ead23de5cf10bf8239c08f8e09e335e2cc602 Mon Sep 17 00:00:00 2001
From: Eduard Heimbuch
Date: Tue, 28 Jul 2020 17:52:20 +0200
Subject: [PATCH 11/46] show signature key on changeset
---
lerna.json | 2 +-
.../scm/api/v2/resources/ChangesetDto.java | 3 +
.../java/sonia/scm/repository/Signature.java | 5 +-
.../sonia/scm/repository/SignatureStatus.java | 29 ++
.../api/RepositoryServiceFactory.java | 148 +++----
.../java/sonia/scm/security/PublicKey.java | 14 +-
.../scm/security/PublicKeyDeletedEvent.java | 35 ++
scm-plugins/scm-git-plugin/package.json | 4 +-
.../scm/repository/GitChangesetConverter.java | 16 +-
.../repository/GitChangesetConverterTest.java | 9 +-
.../sonia/scm/repository/GitTestHelper.java | 1 +
scm-plugins/scm-hg-plugin/package.json | 4 +-
scm-plugins/scm-legacy-plugin/package.json | 4 +-
scm-plugins/scm-svn-plugin/package.json | 4 +-
scm-ui/ui-components/package.json | 4 +-
.../src/__snapshots__/storyshots.test.ts.snap | 410 ++++++++++--------
.../src/repos/changesets/ChangesetRow.tsx | 31 +-
scm-ui/ui-plugins/package.json | 6 +-
scm-ui/ui-styles/package.json | 2 +-
scm-ui/ui-types/package.json | 2 +-
scm-ui/ui-types/src/Changesets.ts | 9 +
scm-ui/ui-webapp/package.json | 4 +-
scm-ui/ui-webapp/public/locales/de/repos.json | 6 +
scm-ui/ui-webapp/public/locales/en/repos.json | 6 +
.../changesets/ChangesetDetails.tsx | 36 +-
.../components/changesets/SignatureIcon.tsx | 79 ++++
.../sonia/scm/security/gpg/DefaultGPG.java | 18 +-
.../java/sonia/scm/security/gpg/GpgKey.java | 25 +-
.../java/sonia/scm/security/gpg/Keys.java | 71 ++-
.../scm/security/gpg/MasterKeyReference.java | 42 ++
.../scm/security/gpg/PublicKeyStore.java | 39 +-
.../scm/security/gpg/DefaultGPGTest.java | 21 +-
.../sonia/scm/security/gpg/GpgKeyTest.java | 6 +-
.../java/sonia/scm/security/gpg/KeysTest.java | 42 +-
.../scm/security/gpg/PublicKeyStoreTest.java | 13 +-
.../sonia/scm/security/gpg/signature.asc | 16 +
yarn.lock | 25 +-
37 files changed, 806 insertions(+), 385 deletions(-)
create mode 100644 scm-core/src/main/java/sonia/scm/repository/SignatureStatus.java
create mode 100644 scm-core/src/main/java/sonia/scm/security/PublicKeyDeletedEvent.java
create mode 100644 scm-ui/ui-webapp/src/repos/components/changesets/SignatureIcon.tsx
create mode 100644 scm-webapp/src/main/java/sonia/scm/security/gpg/MasterKeyReference.java
create mode 100644 scm-webapp/src/test/resources/sonia/scm/security/gpg/signature.asc
diff --git a/lerna.json b/lerna.json
index dd2664e01c..3111260147 100644
--- a/lerna.json
+++ b/lerna.json
@@ -5,5 +5,5 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
- "version": "2.3.0"
+ "version": "2.4.0-SNAPSHOT"
}
diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java
index bd94f9e33f..2a3db03b67 100644
--- a/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java
+++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java
@@ -30,6 +30,7 @@ import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
+import sonia.scm.repository.Signature;
import java.time.Instant;
import java.util.List;
@@ -61,6 +62,8 @@ public class ChangesetDto extends HalRepresentation {
private List contributors;
+ private List signatures;
+
public ChangesetDto(Links links, Embedded embedded) {
super(links, embedded);
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/Signature.java b/scm-core/src/main/java/sonia/scm/repository/Signature.java
index b077793f7b..0f9a4f996a 100644
--- a/scm-core/src/main/java/sonia/scm/repository/Signature.java
+++ b/scm-core/src/main/java/sonia/scm/repository/Signature.java
@@ -28,9 +28,11 @@ import lombok.Value;
import java.io.Serializable;
import java.util.Optional;
+import java.util.Set;
/**
* Signature is the output of a signature verification.
+ *
* @since 2.4.0
*/
@Value
@@ -40,8 +42,9 @@ public class Signature implements Serializable {
private final String keyId;
private final String type;
- private final boolean verified;
+ private final SignatureStatus status;
private final String owner;
+ private final Set contacts;
public Optional getOwner() {
return Optional.ofNullable(owner);
diff --git a/scm-core/src/main/java/sonia/scm/repository/SignatureStatus.java b/scm-core/src/main/java/sonia/scm/repository/SignatureStatus.java
new file mode 100644
index 0000000000..d8784c7a71
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/SignatureStatus.java
@@ -0,0 +1,29 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.repository;
+
+public enum SignatureStatus {
+ VERIFIED, NOT_FOUND, INVALID;
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java
index 31ca9ca15d..fdc936af1f 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java
@@ -55,6 +55,7 @@ import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.spi.RepositoryServiceProvider;
import sonia.scm.repository.spi.RepositoryServiceResolver;
import sonia.scm.repository.work.WorkdirProvider;
+import sonia.scm.security.PublicKeyDeletedEvent;
import sonia.scm.security.ScmSecurityException;
import java.util.Set;
@@ -100,14 +101,12 @@ import static sonia.scm.NotFoundException.notFound;
*
*
* @author Sebastian Sdorra
- * @since 1.17
- *
* @apiviz.landmark
* @apiviz.uses sonia.scm.repository.api.RepositoryService
+ * @since 1.17
*/
@Singleton
-public final class RepositoryServiceFactory
-{
+public final class RepositoryServiceFactory {
/**
* the logger for RepositoryServiceFactory
@@ -122,12 +121,11 @@ public final class RepositoryServiceFactory
* should not be called manually, it should only be used by the injection
* container.
*
- *
- * @param configuration configuration
- * @param cacheManager cache manager
+ * @param configuration configuration
+ * @param cacheManager cache manager
* @param repositoryManager manager for repositories
- * @param resolvers a set of {@link RepositoryServiceResolver}
- * @param preProcessorUtil helper object for pre processor handling
+ * @param resolvers a set of {@link RepositoryServiceResolver}
+ * @param preProcessorUtil helper object for pre processor handling
* @param protocolProviders
* @param workdirProvider
* @since 1.21
@@ -136,8 +134,7 @@ public final class RepositoryServiceFactory
public RepositoryServiceFactory(ScmConfiguration configuration,
CacheManager cacheManager, RepositoryManager repositoryManager,
Set resolvers, PreProcessorUtil preProcessorUtil,
- Set protocolProviders, WorkdirProvider workdirProvider)
- {
+ Set protocolProviders, WorkdirProvider workdirProvider) {
this(
configuration, cacheManager, repositoryManager, resolvers,
preProcessorUtil, protocolProviders, workdirProvider, ScmEventBus.getInstance()
@@ -146,11 +143,10 @@ public final class RepositoryServiceFactory
@VisibleForTesting
RepositoryServiceFactory(ScmConfiguration configuration,
- CacheManager cacheManager, RepositoryManager repositoryManager,
- Set resolvers, PreProcessorUtil preProcessorUtil,
- Set protocolProviders, WorkdirProvider workdirProvider,
- ScmEventBus eventBus)
- {
+ CacheManager cacheManager, RepositoryManager repositoryManager,
+ Set resolvers, PreProcessorUtil preProcessorUtil,
+ Set protocolProviders, WorkdirProvider workdirProvider,
+ ScmEventBus eventBus) {
this.configuration = configuration;
this.cacheManager = cacheManager;
this.repositoryManager = repositoryManager;
@@ -167,19 +163,16 @@ public final class RepositoryServiceFactory
/**
* Creates a new RepositoryService for the given repository.
*
- *
* @param repositoryId id of the repository
- *
* @return a implementation of RepositoryService
- * for the given type of repository
- *
- * @throws NotFoundException if no repository
- * with the given id is available
+ * for the given type of repository
+ * @throws NotFoundException if no repository
+ * with the given id is available
* @throws RepositoryServiceNotFoundException if no repository service
- * implementation for this kind of repository is available
- * @throws IllegalArgumentException if the repository id is null or empty
- * @throws ScmSecurityException if current user has not read permissions
- * for that repository
+ * implementation for this kind of repository is available
+ * @throws IllegalArgumentException if the repository id is null or empty
+ * @throws ScmSecurityException if current user has not read permissions
+ * for that repository
*/
public RepositoryService create(String repositoryId) {
Preconditions.checkArgument(!Strings.isNullOrEmpty(repositoryId),
@@ -187,8 +180,7 @@ public final class RepositoryServiceFactory
Repository repository = repositoryManager.get(repositoryId);
- if (repository == null)
- {
+ if (repository == null) {
throw new NotFoundException(Repository.class, repositoryId);
}
@@ -198,29 +190,24 @@ public final class RepositoryServiceFactory
/**
* Creates a new RepositoryService for the given repository.
*
- *
* @param namespaceAndName namespace and name of the repository
- *
* @return a implementation of RepositoryService
- * for the given type of repository
- *
- * @throws NotFoundException if no repository
- * with the given id is available
+ * for the given type of repository
+ * @throws NotFoundException if no repository
+ * with the given id is available
* @throws RepositoryServiceNotFoundException if no repository service
- * implementation for this kind of repository is available
- * @throws IllegalArgumentException if one of the parameters is null or empty
- * @throws ScmSecurityException if current user has not read permissions
- * for that repository
+ * implementation for this kind of repository is available
+ * @throws IllegalArgumentException if one of the parameters is null or empty
+ * @throws ScmSecurityException if current user has not read permissions
+ * for that repository
*/
- public RepositoryService create(NamespaceAndName namespaceAndName)
- {
+ public RepositoryService create(NamespaceAndName namespaceAndName) {
Preconditions.checkArgument(namespaceAndName != null,
"a non empty namespace and name is required");
Repository repository = repositoryManager.get(namespaceAndName);
- if (repository == null)
- {
+ if (repository == null) {
throw notFound(entity(namespaceAndName));
}
@@ -230,20 +217,16 @@ public final class RepositoryServiceFactory
/**
* Creates a new RepositoryService for the given repository.
*
- *
* @param repository the repository
- *
* @return a implementation of RepositoryService
- * for the given type of repository
- *
+ * for the given type of repository
* @throws RepositoryServiceNotFoundException if no repository service
- * implementation for this kind of repository is available
- * @throws NullPointerException if the repository is null
- * @throws ScmSecurityException if current user has not read permissions
- * for that repository
+ * implementation for this kind of repository is available
+ * @throws NullPointerException if the repository is null
+ * @throws ScmSecurityException if current user has not read permissions
+ * for that repository
*/
- public RepositoryService create(Repository repository)
- {
+ public RepositoryService create(Repository repository) {
Preconditions.checkNotNull(repository, "repository is required");
// check for read permissions of current user
@@ -251,14 +234,11 @@ public final class RepositoryServiceFactory
RepositoryService service = null;
- for (RepositoryServiceResolver resolver : resolvers)
- {
+ for (RepositoryServiceResolver resolver : resolvers) {
RepositoryServiceProvider provider = resolver.resolve(repository);
- if (provider != null)
- {
- if (logger.isDebugEnabled())
- {
+ if (provider != null) {
+ if (logger.isDebugEnabled()) {
logger.debug(
"create new repository service for repository {} of type {}",
repository.getName(), repository.getType());
@@ -271,8 +251,7 @@ public final class RepositoryServiceFactory
}
}
- if (service == null)
- {
+ if (service == null) {
throw new RepositoryServiceNotFoundException(repository);
}
@@ -284,8 +263,7 @@ public final class RepositoryServiceFactory
/**
* Hook and listener to clear all relevant repository caches.
*/
- private static class CacheClearHook
- {
+ private static class CacheClearHook {
private final Set> caches = Sets.newHashSet();
private final CacheManager cacheManager;
@@ -296,8 +274,7 @@ public final class RepositoryServiceFactory
*
* @param cacheManager cache manager
*/
- public CacheClearHook(CacheManager cacheManager)
- {
+ public CacheClearHook(CacheManager cacheManager) {
this.cacheManager = cacheManager;
this.caches.add(cacheManager.getCache(BlameCommandBuilder.CACHE_NAME));
this.caches.add(cacheManager.getCache(BrowseCommandBuilder.CACHE_NAME));
@@ -324,12 +301,10 @@ public final class RepositoryServiceFactory
* @param event hook event
*/
@Subscribe(referenceType = ReferenceType.STRONG)
- public void onEvent(PostReceiveRepositoryHookEvent event)
- {
+ public void onEvent(PostReceiveRepositoryHookEvent event) {
Repository repository = event.getRepository();
- if (repository != null)
- {
+ if (repository != null) {
String id = repository.getId();
clearCaches(id);
@@ -342,10 +317,8 @@ public final class RepositoryServiceFactory
* @param event repository event
*/
@Subscribe(referenceType = ReferenceType.STRONG)
- public void onEvent(RepositoryEvent event)
- {
- if (event.getEventType() == HandlerEventType.DELETE)
- {
+ public void onEvent(RepositoryEvent event) {
+ if (event.getEventType() == HandlerEventType.DELETE) {
clearCaches(event.getItem().getId());
}
}
@@ -357,11 +330,14 @@ public final class RepositoryServiceFactory
cacheManager.getCache(BranchesCommandBuilder.CACHE_NAME).removeAll(predicate);
}
+ @Subscribe
+ public void onEvent(PublicKeyDeletedEvent event) {
+ cacheManager.getCache(LogCommandBuilder.CACHE_NAME).clear();
+ }
+
@SuppressWarnings({"unchecked", "java:S3740", "rawtypes"})
- private void clearCaches(final String repositoryId)
- {
- if (logger.isDebugEnabled())
- {
+ private void clearCaches(final String repositoryId) {
+ if (logger.isDebugEnabled()) {
logger.debug("clear caches for repository id {}", repositoryId);
}
@@ -375,19 +351,29 @@ public final class RepositoryServiceFactory
//~--- fields ---------------------------------------------------------------
- /** cache manager */
+ /**
+ * cache manager
+ */
private final CacheManager cacheManager;
- /** scm-manager configuration */
+ /**
+ * scm-manager configuration
+ */
private final ScmConfiguration configuration;
- /** pre processor util */
+ /**
+ * pre processor util
+ */
private final PreProcessorUtil preProcessorUtil;
- /** repository manager */
+ /**
+ * repository manager
+ */
private final RepositoryManager repositoryManager;
- /** service resolvers */
+ /**
+ * service resolvers
+ */
private final Set resolvers;
private Set protocolProviders;
diff --git a/scm-core/src/main/java/sonia/scm/security/PublicKey.java b/scm-core/src/main/java/sonia/scm/security/PublicKey.java
index 734b19afc1..6348f73509 100644
--- a/scm-core/src/main/java/sonia/scm/security/PublicKey.java
+++ b/scm-core/src/main/java/sonia/scm/security/PublicKey.java
@@ -27,6 +27,7 @@ package sonia.scm.security;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Optional;
+import java.util.Set;
/**
* The public key can be used to verify signatures.
@@ -49,9 +50,17 @@ public interface PublicKey {
*/
Optional getOwner();
+ /**
+ * Returns the contacts of the publickey.
+ *
+ * @return owner or empty optional
+ */
+ Set getContacts();
+
/**
* Verifies that the signature is valid for the given data.
- * @param stream stream of data to verify
+ *
+ * @param stream stream of data to verify
* @param signature signature
* @return {@code true} if the signature is valid for the given data
*/
@@ -59,7 +68,8 @@ public interface PublicKey {
/**
* Verifies that the signature is valid for the given data.
- * @param data data to verify
+ *
+ * @param data data to verify
* @param signature signature
* @return {@code true} if the signature is valid for the given data
*/
diff --git a/scm-core/src/main/java/sonia/scm/security/PublicKeyDeletedEvent.java b/scm-core/src/main/java/sonia/scm/security/PublicKeyDeletedEvent.java
new file mode 100644
index 0000000000..f3579dc7b1
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/security/PublicKeyDeletedEvent.java
@@ -0,0 +1,35 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security;
+
+import sonia.scm.event.Event;
+
+/**
+ * This event is fired when a public key was removed from SCM-Manager.
+ * @since 2.4.0
+ */
+@Event
+public class PublicKeyDeletedEvent {
+}
diff --git a/scm-plugins/scm-git-plugin/package.json b/scm-plugins/scm-git-plugin/package.json
index 4c8fbd8cc0..0f9d2f7d9d 100644
--- a/scm-plugins/scm-git-plugin/package.json
+++ b/scm-plugins/scm-git-plugin/package.json
@@ -1,7 +1,7 @@
{
"name": "@scm-manager/scm-git-plugin",
"private": true,
- "version": "2.3.0",
+ "version": "2.4.0-SNAPSHOT",
"license": "MIT",
"main": "./src/main/js/index.ts",
"scripts": {
@@ -20,6 +20,6 @@
},
"prettier": "@scm-manager/prettier-config",
"dependencies": {
- "@scm-manager/ui-plugins": "^2.3.0"
+ "@scm-manager/ui-plugins": "^2.4.0-SNAPSHOT"
}
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java
index 779fab0dbf..4573c61d87 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java
@@ -26,6 +26,7 @@ package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
+import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import org.eclipse.jgit.lib.ObjectId;
@@ -51,7 +52,6 @@ import java.util.Optional;
//~--- JDK imports ------------------------------------------------------------
/**
- *
* @author Sebastian Sdorra
*/
public class GitChangesetConverter implements Closeable {
@@ -137,11 +137,15 @@ public class GitChangesetConverter implements Closeable {
byte[] signature = Arrays.copyOfRange(raw, start, end);
String publicKeyId = gpg.findPublicKeyId(signature);
+ if (Strings.isNullOrEmpty(publicKeyId)) {
+ // key not found
+ return new Signature(publicKeyId, "gpg", SignatureStatus.NOT_FOUND, null, Collections.emptySet());
+ }
Optional publicKeyById = gpg.findPublicKey(publicKeyId);
if (!publicKeyById.isPresent()) {
// key not found
- return new Signature(publicKeyId, "gpg", false, null);
+ return new Signature(publicKeyId, "gpg", SignatureStatus.NOT_FOUND, null, Collections.emptySet());
}
PublicKey publicKey = publicKeyById.get();
@@ -159,7 +163,13 @@ public class GitChangesetConverter implements Closeable {
}
boolean verified = publicKey.verify(baos.toByteArray(), signature);
- return new Signature(publicKeyId, "gpg", verified, publicKey.getOwner().orElse(null));
+ return new Signature(
+ publicKeyId,
+ "gpg",
+ verified ? SignatureStatus.VERIFIED : SignatureStatus.INVALID,
+ publicKey.getOwner().orElse(null),
+ publicKey.getContacts()
+ );
}
public Person createPersonFor(PersonIdent personIndent) {
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitChangesetConverterTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitChangesetConverterTest.java
index 3a2a624fb5..fcaba7a211 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitChangesetConverterTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitChangesetConverterTest.java
@@ -45,7 +45,6 @@ import org.eclipse.jgit.lib.GpgSignature;
import org.eclipse.jgit.lib.GpgSigner;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
@@ -72,6 +71,7 @@ import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Security;
+import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Optional;
@@ -165,7 +165,7 @@ class GitChangesetConverterTest {
when(gpg.findPublicKeyId(any())).thenReturn(identity);
Signature signature = addSignedCommitAndReturnSignature(identity);
- assertThat(signature).isEqualTo(new Signature(identity, "gpg", false, null));
+ assertThat(signature).isEqualTo(new Signature(identity, "gpg", SignatureStatus.NOT_FOUND, null, Collections.emptySet()));
}
@Test
@@ -175,7 +175,7 @@ class GitChangesetConverterTest {
setPublicKey(identity, owner, false);
Signature signature = addSignedCommitAndReturnSignature(identity);
- assertThat(signature).isEqualTo(new Signature(identity, "gpg", false, owner));
+ assertThat(signature).isEqualTo(new Signature(identity, "gpg", SignatureStatus.INVALID, owner, Collections.emptySet()));
}
@Test
@@ -185,7 +185,7 @@ class GitChangesetConverterTest {
setPublicKey(identity, owner, true);
Signature signature = addSignedCommitAndReturnSignature(identity);
- assertThat(signature).isEqualTo(new Signature(identity, "gpg", true, owner));
+ assertThat(signature).isEqualTo(new Signature(identity, "gpg", SignatureStatus.VERIFIED, owner, Collections.emptySet()));
}
@Test
@@ -241,6 +241,7 @@ class GitChangesetConverterTest {
}
+
private PGPKeyPair createKeyPair() throws PGPException, NoSuchProviderException, NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
// we use a small key size to speedup test, a much larger size should be used for production
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitTestHelper.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitTestHelper.java
index cdf70db55d..e9d36609cb 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitTestHelper.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitTestHelper.java
@@ -30,6 +30,7 @@ import sonia.scm.security.PublicKey;
import java.util.Collections;
import java.util.Optional;
+import java.util.Set;
public final class GitTestHelper {
diff --git a/scm-plugins/scm-hg-plugin/package.json b/scm-plugins/scm-hg-plugin/package.json
index 237ce626e3..af7f6e41ad 100644
--- a/scm-plugins/scm-hg-plugin/package.json
+++ b/scm-plugins/scm-hg-plugin/package.json
@@ -1,7 +1,7 @@
{
"name": "@scm-manager/scm-hg-plugin",
"private": true,
- "version": "2.3.0",
+ "version": "2.4.0-SNAPSHOT",
"license": "MIT",
"main": "./src/main/js/index.ts",
"scripts": {
@@ -19,6 +19,6 @@
},
"prettier": "@scm-manager/prettier-config",
"dependencies": {
- "@scm-manager/ui-plugins": "^2.3.0"
+ "@scm-manager/ui-plugins": "^2.4.0-SNAPSHOT"
}
}
diff --git a/scm-plugins/scm-legacy-plugin/package.json b/scm-plugins/scm-legacy-plugin/package.json
index 4733ac70a7..badb215e52 100644
--- a/scm-plugins/scm-legacy-plugin/package.json
+++ b/scm-plugins/scm-legacy-plugin/package.json
@@ -1,7 +1,7 @@
{
"name": "@scm-manager/scm-legacy-plugin",
"private": true,
- "version": "2.3.0",
+ "version": "2.4.0-SNAPSHOT",
"license": "MIT",
"main": "./src/main/js/index.tsx",
"scripts": {
@@ -19,6 +19,6 @@
},
"prettier": "@scm-manager/prettier-config",
"dependencies": {
- "@scm-manager/ui-plugins": "^2.3.0"
+ "@scm-manager/ui-plugins": "^2.4.0-SNAPSHOT"
}
}
diff --git a/scm-plugins/scm-svn-plugin/package.json b/scm-plugins/scm-svn-plugin/package.json
index ca3b81cfed..907a9d670c 100644
--- a/scm-plugins/scm-svn-plugin/package.json
+++ b/scm-plugins/scm-svn-plugin/package.json
@@ -1,7 +1,7 @@
{
"name": "@scm-manager/scm-svn-plugin",
"private": true,
- "version": "2.3.0",
+ "version": "2.4.0-SNAPSHOT",
"license": "MIT",
"main": "./src/main/js/index.ts",
"scripts": {
@@ -19,6 +19,6 @@
},
"prettier": "@scm-manager/prettier-config",
"dependencies": {
- "@scm-manager/ui-plugins": "^2.3.0"
+ "@scm-manager/ui-plugins": "^2.4.0-SNAPSHOT"
}
}
diff --git a/scm-ui/ui-components/package.json b/scm-ui/ui-components/package.json
index 74dce43cd0..153dae9c17 100644
--- a/scm-ui/ui-components/package.json
+++ b/scm-ui/ui-components/package.json
@@ -1,6 +1,6 @@
{
"name": "@scm-manager/ui-components",
- "version": "2.3.0",
+ "version": "2.4.0-SNAPSHOT",
"description": "UI Components for SCM-Manager and its plugins",
"main": "src/index.ts",
"files": [
@@ -47,7 +47,7 @@
},
"dependencies": {
"@scm-manager/ui-extensions": "^2.1.0",
- "@scm-manager/ui-types": "^2.3.0",
+ "@scm-manager/ui-types": "^2.4.0-SNAPSHOT",
"classnames": "^2.2.6",
"date-fns": "^2.4.1",
"gitdiff-parser": "^0.1.2",
diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap
index 58725b1514..69584d643e 100644
--- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap
+++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap
@@ -1765,58 +1765,62 @@ exports[`Storyshots Changesets Co-Authors with avatar 1`] = `
-
diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/SignatureIcon.tsx b/scm-ui/ui-webapp/src/repos/components/changesets/SignatureIcon.tsx
new file mode 100644
index 0000000000..7bd4148200
--- /dev/null
+++ b/scm-ui/ui-webapp/src/repos/components/changesets/SignatureIcon.tsx
@@ -0,0 +1,79 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+import React, { FC } from "react";
+import { Icon, Tooltip } from "@scm-manager/ui-components";
+import { useTranslation } from "react-i18next";
+//import { Signature } from "@scm-manager/ui-types";
+
+type Props = {
+ signatures: any[];
+ className: any;
+};
+
+const SignatureIcon: FC = ({ signatures, className }) => {
+ const [t] = useTranslation("repos");
+
+ const signature = signatures?.length > 0 ? signatures[0] : undefined;
+
+ const createTooltipMessage = () => {
+ let status;
+ if (signature.status === "VERIFIED") {
+ status = t("changeset.signatureVerified");
+ } else if (signature.status === "INVALID") {
+ status = t("changeset.signatureInvalid");
+ } else {
+ status = t("changeset.signatureNotVerified");
+ }
+
+ if (signature.status === "NOT_FOUND") {
+ return `${t("changeset.signatureStatus")}: ${status}`;
+ }
+
+ return `${t("changeset.signedBy")}: ${signature.owner}\n${t("changeset.keyId")}: ${signature.keyId}\n${t(
+ "changeset.signatureStatus"
+ )}: ${status}\n${t("changeset.keyKontacts")}: ${signature.contacts.map((contact: string) => `\n- ${contact}`)}`;
+ };
+
+ const getColor = () => {
+ if (signature.status === "VERIFIED") {
+ return "success";
+ }
+ if (signature.status === "INVALID") {
+ return "danger";
+ }
+ return undefined;
+ };
+
+ if (!signature) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default SignatureIcon;
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/DefaultGPG.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/DefaultGPG.java
index 792610e30c..ffbf41360f 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/gpg/DefaultGPG.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/DefaultGPG.java
@@ -24,7 +24,10 @@
package sonia.scm.security.gpg;
-import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.openpgp.PGPObjectFactory;
+import org.bouncycastle.openpgp.PGPSignatureList;
+import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.security.GPG;
@@ -32,6 +35,7 @@ import sonia.scm.security.PrivateKey;
import sonia.scm.security.PublicKey;
import javax.inject.Inject;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
@@ -51,8 +55,11 @@ public class DefaultGPG implements GPG {
@Override
public String findPublicKeyId(byte[] signature) {
try {
- return Keys.resolveIdFromKey(new String(signature));
- } catch (PGPException | IOException e) {
+ ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(signature));
+ PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(armoredInputStream, new JcaKeyFingerprintCalculator());
+ PGPSignatureList signatures = (PGPSignatureList) pgpObjectFactory.nextObject();
+ return "0x" + Long.toHexString(signatures.get(0).getKeyID()).toUpperCase();
+ } catch (IOException e) {
LOG.error("Could not find public key id in signature");
}
return "";
@@ -61,7 +68,8 @@ public class DefaultGPG implements GPG {
@Override
public Optional findPublicKey(String id) {
Optional key = store.findById(id);
- return key.map(rawGpgKey -> new GpgKey(id, rawGpgKey.getOwner()));
+
+ return key.map(rawGpgKey -> new GpgKey(id, rawGpgKey.getOwner(), rawGpgKey.getRaw().getBytes()));
}
@Override
@@ -71,7 +79,7 @@ public class DefaultGPG implements GPG {
if (!keys.isEmpty()) {
return keys
.stream()
- .map(rawGpgKey -> new GpgKey(rawGpgKey.getId(), rawGpgKey.getOwner()))
+ .map(rawGpgKey -> new GpgKey(rawGpgKey.getId(), rawGpgKey.getOwner(), rawGpgKey.getRaw().getBytes()))
.collect(Collectors.toSet());
}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java
index 26a903744b..9fdbfca991 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java
@@ -43,7 +43,9 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashSet;
import java.util.Optional;
+import java.util.Set;
public class GpgKey implements PublicKey {
@@ -51,10 +53,16 @@ public class GpgKey implements PublicKey {
private final String id;
private final String owner;
+ private final Set contacts = new LinkedHashSet<>();
- public GpgKey(String id, String owner) {
+ public GpgKey(String id, String owner, byte[] raw) {
this.id = id;
this.owner = owner;
+ try {
+ getPgpPublicKey(raw).getUserIDs().forEachRemaining(contacts::add);
+ } catch (IOException e) {
+ LOG.error("Could not find contacts in public key", e);
+ }
}
@Override
@@ -70,13 +78,16 @@ public class GpgKey implements PublicKey {
return Optional.of(owner);
}
+ @Override
+ public Set getContacts() {
+ return contacts;
+ }
+
@Override
public boolean verify(InputStream stream, byte[] signature) {
boolean verified = false;
try {
- ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(signature));
- PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(armoredInputStream, new JcaKeyFingerprintCalculator());
- PGPPublicKey publicKey = ((PGPPublicKeyRing) pgpObjectFactory.nextObject()).getPublicKey();
+ PGPPublicKey publicKey = getPgpPublicKey(signature);
PGPSignature pgpSignature = ((PGPSignature) publicKey.getSignatures().next());
PGPContentVerifierBuilderProvider provider = new JcaPGPContentVerifierBuilderProvider();
@@ -97,6 +108,12 @@ public class GpgKey implements PublicKey {
}
return verified;
}
+
+ private PGPPublicKey getPgpPublicKey(byte[] signature) throws IOException {
+ ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(signature));
+ PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(armoredInputStream, new JcaKeyFingerprintCalculator());
+ return ((PGPPublicKeyRing) pgpObjectFactory.nextObject()).getPublicKey();
+ }
}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/Keys.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/Keys.java
index 26f2d94403..50075c217f 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/gpg/Keys.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/Keys.java
@@ -24,6 +24,7 @@
package sonia.scm.security.gpg;
+import lombok.Value;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
@@ -37,45 +38,71 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
-import java.util.stream.Collectors;
+import java.util.Set;
+import java.util.function.Function;
+@Value
final class Keys {
private static final KeyFingerPrintCalculator calculator = new JcaKeyFingerprintCalculator();
- private Keys() {}
+ private final String master;
+ private final Set subs;
- static String resolveIdFromKey(String rawKey) throws IOException, PGPException {
- List keys = collectKeys(rawKey);
- if (keys.size() > 1) {
- keys = keys.stream().filter(PGPPublicKey::isMasterKey).collect(Collectors.toList());
- }
- if (keys.isEmpty()) {
- throw new IllegalArgumentException("found multiple keys, but no master keys");
- }
- if (keys.size() > 1) {
- throw new IllegalArgumentException("found multiple master keys");
+ private Keys(String master, Set subs) {
+ this.master = master;
+ this.subs = subs;
+ }
+
+ static Keys resolve(String raw) {
+ return resolve(raw, Keys::collectKeys);
+ }
+
+ static Keys resolve(String raw, Function> parser) {
+ List parsedKeys = parser.apply(raw);
+
+ String master = null;
+ Set subs = new HashSet<>();
+
+ for (PGPPublicKey key : parsedKeys) {
+ if (key.isMasterKey()) {
+ if (master != null) {
+ throw new IllegalArgumentException("Found more than one master key");
+ }
+ master = createId(key);
+ } else {
+ subs.add(createId(key));
+ }
}
- PGPPublicKey pgpPublicKey = keys.get(0);
- return createId(pgpPublicKey);
+ if (master == null) {
+ throw new IllegalArgumentException("No master key found");
+ }
+
+ return new Keys(master, Collections.unmodifiableSet(subs));
}
private static String createId(PGPPublicKey pgpPublicKey) {
return "0x" + Long.toHexString(pgpPublicKey.getKeyID()).toUpperCase(Locale.ENGLISH);
}
- private static List collectKeys(String rawKey) throws IOException, PGPException {
- List publicKeys = new ArrayList<>();
- InputStream decoderStream = PGPUtil.getDecoderStream(new ByteArrayInputStream(rawKey.getBytes(StandardCharsets.UTF_8)));
- PGPPublicKeyRingCollection collection = new PGPPublicKeyRingCollection(decoderStream, calculator);
- for (PGPPublicKeyRing pgpPublicKeys : collection) {
- for (PGPPublicKey pgpPublicKey : pgpPublicKeys) {
- publicKeys.add(pgpPublicKey);
+ private static List collectKeys(String rawKey) {
+ try {
+ List publicKeys = new ArrayList<>();
+ InputStream decoderStream = PGPUtil.getDecoderStream(new ByteArrayInputStream(rawKey.getBytes(StandardCharsets.UTF_8)));
+ PGPPublicKeyRingCollection collection = new PGPPublicKeyRingCollection(decoderStream, calculator);
+ for (PGPPublicKeyRing pgpPublicKeys : collection) {
+ for (PGPPublicKey pgpPublicKey : pgpPublicKeys) {
+ publicKeys.add(pgpPublicKey);
+ }
}
+ return publicKeys;
+ } catch (IOException | PGPException ex) {
+ throw new GPGException("Failed to collect public keys", ex);
}
- return publicKeys;
}
}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/MasterKeyReference.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/MasterKeyReference.java
new file mode 100644
index 0000000000..86e5b1df38
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/MasterKeyReference.java
@@ -0,0 +1,42 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlRootElement;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@XmlAccessorType(XmlAccessType.FIELD)
+@XmlRootElement
+public class MasterKeyReference {
+ String masterKey;
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java
index cbfa1b5302..ce3d756781 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java
@@ -24,16 +24,16 @@
package sonia.scm.security.gpg;
-import org.bouncycastle.openpgp.PGPException;
import sonia.scm.ContextEntry;
+import sonia.scm.event.ScmEventBus;
import sonia.scm.security.NotPublicKeyException;
+import sonia.scm.security.PublicKeyDeletedEvent;
import sonia.scm.store.DataStore;
import sonia.scm.store.DataStoreFactory;
import sonia.scm.user.UserPermissions;
import javax.inject.Inject;
import javax.inject.Singleton;
-import java.io.IOException;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
@@ -43,31 +43,39 @@ import java.util.stream.Collectors;
public class PublicKeyStore {
private static final String STORE_NAME = "gpg_public_keys";
+ private static final String SUBKEY_STORE_NAME = "gpg_public_sub_keys";
private final DataStore store;
+ private final DataStore subKeyStore;
+ private final ScmEventBus eventBus;
@Inject
- public PublicKeyStore(DataStoreFactory dataStoreFactory) {
+ public PublicKeyStore(DataStoreFactory dataStoreFactory, ScmEventBus eventBus) {
this.store = dataStoreFactory.withType(RawGpgKey.class).withName(STORE_NAME).build();
+ this.subKeyStore = dataStoreFactory.withType(MasterKeyReference.class).withName(SUBKEY_STORE_NAME).build();
+ this.eventBus = eventBus;
}
public RawGpgKey add(String displayName, String username, String rawKey) {
- UserPermissions.modify(username).check();
+ UserPermissions.changePublicKeys(username).check();
if (!rawKey.contains("PUBLIC KEY")) {
throw new NotPublicKeyException(ContextEntry.ContextBuilder.entity(RawGpgKey.class, displayName).build(), "The provided key is not a public key");
}
- try {
- String id = Keys.resolveIdFromKey(rawKey);
- RawGpgKey key = new RawGpgKey(id, displayName, username, rawKey, Instant.now());
+ Keys keys = Keys.resolve(rawKey);
+ String master = keys.getMaster();
- store.put(id, key);
-
- return key;
- } catch (IOException | PGPException e) {
- throw new GPGException("failed to resolve id from gpg key");
+ for (String subKey : keys.getSubs()) {
+ subKeyStore.put(subKey, new MasterKeyReference(master));
}
+
+ RawGpgKey key = new RawGpgKey(master, displayName, username, rawKey, Instant.now());
+
+ store.put(master, key);
+
+ return key;
+
}
public void delete(String id) {
@@ -75,10 +83,17 @@ public class PublicKeyStore {
if (rawGpgKey != null) {
UserPermissions.modify(rawGpgKey.getOwner()).check();
store.remove(id);
+ eventBus.post(new PublicKeyDeletedEvent());
}
}
public Optional findById(String id) {
+ Optional reference = subKeyStore.getOptional(id);
+
+ if (reference.isPresent()) {
+ return store.getOptional(reference.get().getMasterKey());
+ }
+
return store.getOptional(id);
}
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultGPGTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultGPGTest.java
index 63f39bd523..98cff24a35 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultGPGTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultGPGTest.java
@@ -35,6 +35,7 @@ import sonia.scm.security.PublicKey;
import java.io.IOException;
import java.time.Instant;
import java.util.Optional;
+import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@@ -50,15 +51,16 @@ class DefaultGPGTest {
@Test
void shouldFindIdInSignature() throws IOException {
- String raw = GPGTestHelper.readKey("single.asc");
+ String raw = GPGTestHelper.readKey("signature.asc");
String publicKeyId = gpg.findPublicKeyId(raw.getBytes());
- assertThat(publicKeyId).isEqualTo("0x975922F193B07D6E");
+ assertThat(publicKeyId).isEqualTo("0x1F17B79A09DAD5B9");
}
@Test
- void shouldFindPublicKey() {
- RawGpgKey key1 = new RawGpgKey("42", "key_42", "trillian", "raw", Instant.now());
+ void shouldFindPublicKey() throws IOException {
+ String raw = GPGTestHelper.readKey("subkeys.asc");
+ RawGpgKey key1 = new RawGpgKey("42", "key_42", "trillian", raw, Instant.now());
when(store.findById("42")).thenReturn(Optional.of(key1));
@@ -68,12 +70,17 @@ class DefaultGPGTest {
assertThat(publicKey.get().getOwner()).isPresent();
assertThat(publicKey.get().getOwner().get()).contains("trillian");
assertThat(publicKey.get().getId()).isEqualTo("42");
+ assertThat(publicKey.get().getContacts()).contains("Sebastian Sdorra ",
+ "Sebastian Sdorra ");
}
@Test
- void shouldFindKeysForUsername() {
- RawGpgKey key1 = new RawGpgKey("1", "1", "trillian", "raw", Instant.now());
- RawGpgKey key2 = new RawGpgKey("2", "2", "trillian", "raw", Instant.now());
+ void shouldFindKeysForUsername() throws IOException {
+ String raw = GPGTestHelper.readKey("single.asc");
+ String raw2= GPGTestHelper.readKey("subkeys.asc");
+
+ RawGpgKey key1 = new RawGpgKey("1", "1", "trillian", raw, Instant.now());
+ RawGpgKey key2 = new RawGpgKey("2", "2", "trillian", raw2, Instant.now());
when(store.findByUsername("trillian")).thenReturn(ImmutableList.of(key1, key2));
Iterable keys = gpg.findPublicKeysByUsername("trillian");
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java
index 86bf5de4e0..6fb6a3ac69 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java
@@ -28,8 +28,6 @@ import org.junit.jupiter.api.Test;
import java.io.IOException;
-import static org.assertj.core.api.Assertions.assertThat;
-
class GpgKeyTest {
@Test
@@ -41,11 +39,11 @@ class GpgKeyTest {
byte[] raw = GPGTestHelper.readKey("subkeys.asc").getBytes();
- GpgKey key = new GpgKey("1", "trillian");
+ GpgKey key = new GpgKey("1", "trillian", raw);
boolean verified = key.verify(longContent.toString().getBytes(), raw);
- // assertThat(verified).isTrue();
+ // assertThat(verified).isTrue();
}
}
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysTest.java
index 765674d0f2..3bc97eca2a 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysTest.java
@@ -24,26 +24,58 @@
package sonia.scm.security.gpg;
-import org.bouncycastle.openpgp.PGPException;
+import com.google.common.collect.ImmutableList;
+import org.bouncycastle.openpgp.PGPPublicKey;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
import java.io.IOException;
+import java.util.Collections;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
import static sonia.scm.security.gpg.GPGTestHelper.readKey;
+@ExtendWith(MockitoExtension.class)
class KeysTest {
@Test
- void shouldResolveId() throws IOException, PGPException {
+ void shouldResolveSingleId() throws IOException {
String rawPublicKey = readKey("single.asc");
- assertThat(Keys.resolveIdFromKey(rawPublicKey)).isEqualTo("0x975922F193B07D6E");
+ Keys keys = Keys.resolve(rawPublicKey);
+ assertThat(keys.getMaster()).isEqualTo("0x975922F193B07D6E");
}
@Test
- void shouldResolveIdFromMasterKey() throws IOException, PGPException {
+ void shouldResolveIdsFromSubkeys() throws IOException {
String rawPublicKey = readKey("subkeys.asc");
- assertThat(Keys.resolveIdFromKey(rawPublicKey)).isEqualTo("0x13B13D4C8A9350A1");
+ Keys keys = Keys.resolve(rawPublicKey);
+ assertThat(keys.getMaster()).isEqualTo("0x13B13D4C8A9350A1");
+ assertThat(keys.getSubs()).containsOnly("0x247E908C6FD35473", "0xE50E1DD8B90D3A6B", "0xBF49759E43DD0E60");
+ }
+
+ @Test
+ void shouldThrowIllegalArgumentExceptionForMultipleMasterKeys() {
+ PGPPublicKey one = mockMasterKey(42L);
+ PGPPublicKey two = mockMasterKey(21L);
+
+ assertThrows(IllegalArgumentException.class, () -> Keys.resolve("", raw -> ImmutableList.of(one, two)));
+ }
+
+ @Test
+ void shouldThrowIllegalArgumentExceptionWithoutMasterKey() {
+ assertThrows(IllegalArgumentException.class, () -> Keys.resolve("", raw -> Collections.emptyList()));
+ }
+
+ private PGPPublicKey mockMasterKey(long id) {
+ PGPPublicKey key = mock(PGPPublicKey.class);
+ when(key.isMasterKey()).thenReturn(true);
+ lenient().when(key.getKeyID()).thenReturn(id);
+ return key;
}
}
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java
index 1b4e20cffc..1871827939 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java
@@ -33,7 +33,9 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
+import sonia.scm.event.ScmEventBus;
import sonia.scm.security.NotPublicKeyException;
+import sonia.scm.security.PublicKeyDeletedEvent;
import sonia.scm.store.DataStoreFactory;
import sonia.scm.store.InMemoryDataStoreFactory;
@@ -44,7 +46,9 @@ import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
class PublicKeyStoreTest {
@@ -52,12 +56,15 @@ class PublicKeyStoreTest {
@Mock
private Subject subject;
+ @Mock
+ private ScmEventBus eventBus;
+
private PublicKeyStore keyStore;
private final DataStoreFactory dataStoreFactory = new InMemoryDataStoreFactory();
@BeforeEach
void setUpKeyStore() {
- keyStore = new PublicKeyStore(dataStoreFactory);
+ keyStore = new PublicKeyStore(dataStoreFactory, eventBus);
}
@BeforeEach
@@ -72,7 +79,7 @@ class PublicKeyStoreTest {
@Test
void shouldThrowAuthorizationExceptionOnAdd() throws IOException {
- doThrow(AuthorizationException.class).when(subject).checkPermission("user:modify:zaphod");
+ doThrow(AuthorizationException.class).when(subject).checkPermission("user:changePublicKeys:zaphod");
String rawKey = GPGTestHelper.readKey("single.asc");
assertThrows(AuthorizationException.class, () -> keyStore.add("zaphods key", "zaphod", rawKey));
@@ -118,6 +125,8 @@ class PublicKeyStoreTest {
key = keyStore.findById("0x975922F193B07D6E");
assertThat(key).isNotPresent();
+
+ verify(eventBus).post(any(PublicKeyDeletedEvent.class));
}
@Test
diff --git a/scm-webapp/src/test/resources/sonia/scm/security/gpg/signature.asc b/scm-webapp/src/test/resources/sonia/scm/security/gpg/signature.asc
new file mode 100644
index 0000000000..f3e756a16d
--- /dev/null
+++ b/scm-webapp/src/test/resources/sonia/scm/security/gpg/signature.asc
@@ -0,0 +1,16 @@
+-----BEGIN PGP SIGNATURE-----
+
+ iQIzBAABCgAdFiEEAibrQDifYryAFPjQHxe3mgna1bkFAl8gSFMACgkQHxe3mgna
+ 1bk0Hg/9HaN7aRkRo1FH5xPnswGFAidHG+XQBFYTK3EhR2g6m4iRCij58qIEMQVh
+ gV6FTtz6xGB/Oq32e5+gp9dYTp6lTrfm35hjg7uzwiC7OQx3QswZn7GUX/ZmuLk0
+ nqz4ryiVxeMWst47JkKAm9PY6GC+UITaL3tptNF//MBPAwEfNnDP7O667BTvnROh
+ 9XaSlIdYlbGxs5YzQK8BMB56YdutTDMtHl92oOjDA7r238dScmlNUSw3IQIsNPkz
+ 4WZwGM7HVxw7PjbRoMbwXbNEJ87F4SuBqhWM7BjHEUFS21wH8APtVZrNzd2znveq
+ oemn/+9pn0LG3Mg/2FASqHA6X+HS79YT9fb0O3HUvHzlaJmev88h4JSGcTJZG5+o
+ a9LEPW56clJYmCq/ghKAyV+bJfUkIAP9i75p4zi8Il4ACJnf9oVRg6RuOTXK5cnf
+ bvSzEtBWlXT1ELV52uo9gwcQMqgkQ89p8pTHcYHD3UhfdsuCfzlTGP/aQ/6OUBGg
+ k6AFS1kAwalAp2VEOBIJUXylM60VfdLaLfWpgg4T8mq4WIV3ROXTV0o2XciuF4r0
+ 2oXtyc84J7nnGJlJ4HqHBqzMNHF4giqVNhKQzBAQ1OEjePBfkW6RMDcYxkLilUrU
+ LzguRD1ybuS9xYQK60s2S68sNoAFkY8NTn9z8je1BO7ajzQn/fw=
+ =smHD
+ -----END PGP SIGNATURE-----
diff --git a/yarn.lock b/yarn.lock
index 2b7e82ef4d..742b29d0cd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -984,6 +984,13 @@
dependencies:
regenerator-runtime "^0.13.4"
+"@babel/runtime@^7.10.1":
+ version "7.10.5"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.5.tgz#303d8bd440ecd5a491eae6117fd3367698674c5c"
+ integrity sha512-otddXKhdNn7d0ptoFRHtMLa8LqDxLYwTjB4nYgM1yy5N6gU/MUf8zqyyLltCH3yAVitBzmwK4us+DD0l/MauAg==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@babel/template@^7.10.1", "@babel/template@^7.3.3":
version "7.10.1"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811"
@@ -4917,10 +4924,10 @@ bulma-tooltip@^3.0.0:
resolved "https://registry.yarnpkg.com/bulma-tooltip/-/bulma-tooltip-3.0.2.tgz#2cf0abab1de2eba07f9d84eb7f07a8a88819ea92"
integrity sha512-CsT3APjhlZScskFg38n8HYL8oYNUHQtcu4sz6ERarxkUpBRbk9v0h/5KAvXeKapVSn2dp9l7bOGit5SECP8EWQ==
-bulma@^0.8.0:
- version "0.8.2"
- resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.8.2.tgz#5d928f16ed4a84549c2873f95c92c38c69c631a7"
- integrity sha512-vMM/ijYSxX+Sm+nD7Lmc1UgWDy2JcL2nTKqwgEqXuOMU+IGALbXd5MLt/BcjBAPLIx36TtzhzBcSnOP974gcqA==
+bulma@^0.9.0:
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.9.0.tgz#948c5445a49e9d7546f0826cb3820d17178a814f"
+ integrity sha512-rV75CJkubNUroAt0qCRkjznZLoaXq/ctfMXsMvKSL84UetbSyx5REl96e8GoQ04G4Tkw0XF3STECffTOQrbzOQ==
byline@^5.0.0:
version "5.0.0"
@@ -8656,12 +8663,12 @@ i18next@*:
dependencies:
"@babel/runtime" "^7.3.1"
-i18next@^17.3.0:
- version "17.3.1"
- resolved "https://registry.yarnpkg.com/i18next/-/i18next-17.3.1.tgz#5fe75e054aae39a6f38f1a79f7ab49184c6dc7a1"
- integrity sha512-4nY+yaENaoZKmpbiDXPzucVHCN3hN9Z9Zk7LyQXVOKVIpnYOJ3L/yxHJlBPtJDq3PGgjFwA0QBFm/26Z0iDT5A==
+i18next@^19.6.0:
+ version "19.6.3"
+ resolved "https://registry.yarnpkg.com/i18next/-/i18next-19.6.3.tgz#ce2346161b35c4c5ab691b0674119c7b349c0817"
+ integrity sha512-eYr98kw/C5z6kY21ti745p4IvbOJwY8F2T9tf/Lvy5lFnYRqE45+bppSgMPmcZZqYNT+xO0N0x6rexVR2wtZZQ==
dependencies:
- "@babel/runtime" "^7.3.1"
+ "@babel/runtime" "^7.10.1"
iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13:
version "0.4.24"
From 3da77105433e41b173afb3b4c629383b1b34ddd7 Mon Sep 17 00:00:00 2001
From: Eduard Heimbuch
Date: Wed, 29 Jul 2020 17:16:57 +0200
Subject: [PATCH 12/46] refactor
---
.../java/sonia/scm/security/PublicKey.java | 7 ++
scm-ui/ui-webapp/public/locales/de/repos.json | 1 +
scm-ui/ui-webapp/public/locales/en/repos.json | 1 +
.../components/changesets/SignatureIcon.tsx | 10 +-
.../components/publicKeys/formatPublicKey.ts | 19 +--
.../sonia/scm/security/gpg/DefaultGPG.java | 4 +-
.../java/sonia/scm/security/gpg/GpgKey.java | 57 ++++-----
.../security/gpg/PgpPublicKeyExtractor.java | 57 +++++++++
.../scm/security/gpg/PublicKeyStore.java | 18 ++-
.../sonia/scm/security/gpg/RawGpgKey.java | 2 +
.../scm/security/gpg/DefaultGPGTest.java | 20 ++--
.../sonia/scm/security/gpg/GPGTestHelper.java | 4 +-
.../sonia/scm/security/gpg/GpgKeyTest.java | 12 +-
.../java/sonia/scm/security/gpg/KeysTest.java | 6 +-
.../gpg/PgpPublicKeyExtractorTest.java | 47 ++++++++
.../gpg/PublicKeyCollectionMapperTest.java | 5 +-
.../scm/security/gpg/PublicKeyMapperTest.java | 9 +-
.../security/gpg/PublicKeyResourceTest.java | 2 +-
.../scm/security/gpg/PublicKeyStoreTest.java | 14 +--
.../sonia/scm/security/gpg/pubKeyEH.asc | 109 ++++++++++++++++++
20 files changed, 323 insertions(+), 81 deletions(-)
create mode 100644 scm-webapp/src/main/java/sonia/scm/security/gpg/PgpPublicKeyExtractor.java
create mode 100644 scm-webapp/src/test/java/sonia/scm/security/gpg/PgpPublicKeyExtractorTest.java
create mode 100644 scm-webapp/src/test/resources/sonia/scm/security/gpg/pubKeyEH.asc
diff --git a/scm-core/src/main/java/sonia/scm/security/PublicKey.java b/scm-core/src/main/java/sonia/scm/security/PublicKey.java
index 6348f73509..bcce1814fa 100644
--- a/scm-core/src/main/java/sonia/scm/security/PublicKey.java
+++ b/scm-core/src/main/java/sonia/scm/security/PublicKey.java
@@ -50,6 +50,13 @@ public interface PublicKey {
*/
Optional getOwner();
+ /**
+ * Returns raw of the public key.
+ *
+ * @return raw of key
+ */
+ String getRaw();
+
/**
* Returns the contacts of the publickey.
*
diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json
index c711113356..71eb6a9f98 100644
--- a/scm-ui/ui-webapp/public/locales/de/repos.json
+++ b/scm-ui/ui-webapp/public/locales/de/repos.json
@@ -91,6 +91,7 @@
"signedBy": "Signiert von",
"signatureStatus": "Status",
"keyId": "Schlüssel-ID",
+ "keyContacts": "Kontakte",
"signatureVerified": "Verifiziert",
"signatureNotVerified": "Nicht verifiziert",
"signatureInvalid": "Ungültig",
diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json
index 93b846f7a8..a53146bc78 100644
--- a/scm-ui/ui-webapp/public/locales/en/repos.json
+++ b/scm-ui/ui-webapp/public/locales/en/repos.json
@@ -89,6 +89,7 @@
"tags": "Tags",
"signedBy": "Signed by",
"keyId": "Key ID",
+ "keyContacts": "Contacts",
"signatureStatus": "Status",
"signatureVerified": "verified",
"signatureNotVerified": "not verified",
diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/SignatureIcon.tsx b/scm-ui/ui-webapp/src/repos/components/changesets/SignatureIcon.tsx
index 7bd4148200..bd3b4863d8 100644
--- a/scm-ui/ui-webapp/src/repos/components/changesets/SignatureIcon.tsx
+++ b/scm-ui/ui-webapp/src/repos/components/changesets/SignatureIcon.tsx
@@ -50,9 +50,15 @@ const SignatureIcon: FC = ({ signatures, className }) => {
return `${t("changeset.signatureStatus")}: ${status}`;
}
- return `${t("changeset.signedBy")}: ${signature.owner}\n${t("changeset.keyId")}: ${signature.keyId}\n${t(
+ let message = `${t("changeset.signedBy")}: ${signature.owner}\n${t("changeset.keyId")}: ${signature.keyId}\n${t(
"changeset.signatureStatus"
- )}: ${status}\n${t("changeset.keyKontacts")}: ${signature.contacts.map((contact: string) => `\n- ${contact}`)}`;
+ )}: ${status}`;
+
+ if (signature.contacts?.length > 0) {
+ message = message + `\n${t("changeset.keyContacts")}: ${signature.contacts.map((contact: string) => `\n- ${contact}`)}`;
+ }
+
+ return message;
};
const getColor = () => {
diff --git a/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.ts b/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.ts
index aa2db2a14c..af17e1b362 100644
--- a/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.ts
+++ b/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.ts
@@ -23,21 +23,6 @@
*/
export const formatPublicKey = (key: string) => {
- const parts = key.split(/\s+/);
- if (parts.length === 3) {
- return parts[0] + " ... " + parts[2];
- } else if (parts.length === 2) {
- if (parts[0].length >= parts[1].length) {
- return parts[0].substring(0, 7) + "... " + parts[1];
- } else {
- const keyLength = parts[1].length;
- return parts[0] + " ..." + parts[1].substring(keyLength - 7);
- }
- } else {
- const keyLength = parts[0].length;
- if (keyLength < 15) {
- return parts[0];
- }
- return parts[0].substring(0, 7) + "..." + parts[0].substring(keyLength - 7);
- }
+ const parts = key.split(/\n/);
+ return parts[2].substring(0, 15);
};
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/DefaultGPG.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/DefaultGPG.java
index ffbf41360f..008c7f4b5e 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/gpg/DefaultGPG.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/DefaultGPG.java
@@ -69,7 +69,7 @@ public class DefaultGPG implements GPG {
public Optional findPublicKey(String id) {
Optional key = store.findById(id);
- return key.map(rawGpgKey -> new GpgKey(id, rawGpgKey.getOwner(), rawGpgKey.getRaw().getBytes()));
+ return key.map(rawGpgKey -> new GpgKey(rawGpgKey.getId(), rawGpgKey.getOwner(), rawGpgKey.getRaw(), rawGpgKey.getContacts()));
}
@Override
@@ -79,7 +79,7 @@ public class DefaultGPG implements GPG {
if (!keys.isEmpty()) {
return keys
.stream()
- .map(rawGpgKey -> new GpgKey(rawGpgKey.getId(), rawGpgKey.getOwner(), rawGpgKey.getRaw().getBytes()))
+ .map(rawGpgKey -> new GpgKey(rawGpgKey.getId(), rawGpgKey.getOwner(), rawGpgKey.getRaw(), rawGpgKey.getContacts()))
.collect(Collectors.toSet());
}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java
index 9fdbfca991..2855a0d265 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java
@@ -28,10 +28,9 @@ import org.bouncycastle.bcpg.ArmoredInputStream;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPPublicKey;
-import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider;
-import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -43,26 +42,25 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
-import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.Set;
+import static sonia.scm.security.gpg.PgpPublicKeyExtractor.getFromRawKey;
+
public class GpgKey implements PublicKey {
private static final Logger LOG = LoggerFactory.getLogger(GpgKey.class);
private final String id;
private final String owner;
- private final Set contacts = new LinkedHashSet<>();
+ private final String raw;
+ private final Set contacts;
- public GpgKey(String id, String owner, byte[] raw) {
+ public GpgKey(String id, String owner, String raw, Set contacts) {
this.id = id;
this.owner = owner;
- try {
- getPgpPublicKey(raw).getUserIDs().forEachRemaining(contacts::add);
- } catch (IOException e) {
- LOG.error("Could not find contacts in public key", e);
- }
+ this.raw = raw;
+ this.contacts = contacts;
}
@Override
@@ -78,6 +76,11 @@ public class GpgKey implements PublicKey {
return Optional.of(owner);
}
+ @Override
+ public String getRaw() {
+ return raw;
+ }
+
@Override
public Set getContacts() {
return contacts;
@@ -87,32 +90,34 @@ public class GpgKey implements PublicKey {
public boolean verify(InputStream stream, byte[] signature) {
boolean verified = false;
try {
- PGPPublicKey publicKey = getPgpPublicKey(signature);
- PGPSignature pgpSignature = ((PGPSignature) publicKey.getSignatures().next());
+ ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(signature));
+ PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(armoredInputStream, null);
+ PGPSignature pgpSignature = ((PGPSignatureList) pgpObjectFactory.nextObject()).get(0);
PGPContentVerifierBuilderProvider provider = new JcaPGPContentVerifierBuilderProvider();
- pgpSignature.init(provider, publicKey);
- char[] buffer = new char[1024];
- int bytesRead = 0;
- BufferedReader in = new BufferedReader(new InputStreamReader(stream));
+ Optional pgpPublicKey = getFromRawKey(raw);
- while (bytesRead != -1) {
- bytesRead = in.read(buffer, 0, 1024);
- pgpSignature.update(new String(buffer).getBytes(StandardCharsets.UTF_8));
+ if (pgpPublicKey.isPresent()) {
+ pgpSignature.init(provider, pgpPublicKey.get());
+
+ char[] buffer = new char[1024];
+ int bytesRead = 0;
+ BufferedReader in = new BufferedReader(new InputStreamReader(stream));
+
+ while (bytesRead != -1) {
+ bytesRead = in.read(buffer, 0, 1024);
+ pgpSignature.update(new String(buffer).getBytes(StandardCharsets.UTF_8));
+ }
+
+ verified = pgpSignature.verify();
}
- verified = pgpSignature.verify();
} catch (IOException | PGPException e) {
LOG.error("Could not verify GPG key", e);
}
- return verified;
- }
- private PGPPublicKey getPgpPublicKey(byte[] signature) throws IOException {
- ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(signature));
- PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(armoredInputStream, new JcaKeyFingerprintCalculator());
- return ((PGPPublicKeyRing) pgpObjectFactory.nextObject()).getPublicKey();
+ return verified;
}
}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/PgpPublicKeyExtractor.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/PgpPublicKeyExtractor.java
new file mode 100644
index 0000000000..003503194b
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/PgpPublicKeyExtractor.java
@@ -0,0 +1,57 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.openpgp.PGPObjectFactory;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.Optional;
+
+public class PgpPublicKeyExtractor {
+
+ private PgpPublicKeyExtractor() {}
+
+ private static final Logger LOG = LoggerFactory.getLogger(PgpPublicKeyExtractor.class);
+
+ static Optional getFromRawKey(String rawKey) {
+ try {
+ ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(rawKey.getBytes()));
+ PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(armoredInputStream, new JcaKeyFingerprintCalculator());
+ PGPPublicKey publicKey = ((PGPPublicKeyRing) pgpObjectFactory.nextObject()).getPublicKey();
+ return Optional.of(publicKey);
+
+ } catch (IOException e) {
+ LOG.error("Invalid PGP key");
+ }
+ return Optional.empty();
+ }
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java
index ce3d756781..7308ea6cfe 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java
@@ -24,6 +24,9 @@
package sonia.scm.security.gpg;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import sonia.scm.ContextEntry;
import sonia.scm.event.ScmEventBus;
import sonia.scm.security.NotPublicKeyException;
@@ -35,13 +38,19 @@ import sonia.scm.user.UserPermissions;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.time.Instant;
+import java.util.HashSet;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
import java.util.stream.Collectors;
+import static sonia.scm.security.gpg.PgpPublicKeyExtractor.getFromRawKey;
+
@Singleton
public class PublicKeyStore {
+ private static final Logger LOG = LoggerFactory.getLogger(PublicKeyStore.class);
+
private static final String STORE_NAME = "gpg_public_keys";
private static final String SUBKEY_STORE_NAME = "gpg_public_sub_keys";
@@ -70,7 +79,7 @@ public class PublicKeyStore {
subKeyStore.put(subKey, new MasterKeyReference(master));
}
- RawGpgKey key = new RawGpgKey(master, displayName, username, rawKey, Instant.now());
+ RawGpgKey key = new RawGpgKey(master, displayName, username, rawKey, getContactsFromPublicKey(rawKey), Instant.now());
store.put(master, key);
@@ -78,6 +87,13 @@ public class PublicKeyStore {
}
+ private Set getContactsFromPublicKey(String rawKey) {
+ Set contacts = new HashSet<>();
+ Optional publicKeyFromRawKey = getFromRawKey(rawKey);
+ publicKeyFromRawKey.ifPresent(pgpPublicKey -> pgpPublicKey.getUserIDs().forEachRemaining(contacts::add));
+ return contacts;
+ }
+
public void delete(String id) {
RawGpgKey rawGpgKey = store.get(id);
if (rawGpgKey != null) {
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKey.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKey.java
index f32d757d24..19c88296c8 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKey.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKey.java
@@ -36,6 +36,7 @@ import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.time.Instant;
import java.util.Objects;
+import java.util.Set;
@Getter
@NoArgsConstructor
@@ -48,6 +49,7 @@ public class RawGpgKey {
private String displayName;
private String owner;
private String raw;
+ private Set contacts;
@XmlJavaTypeAdapter(XmlInstantAdapter.class)
private Instant created;
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultGPGTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultGPGTest.java
index 98cff24a35..d98aa56b85 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultGPGTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultGPGTest.java
@@ -25,6 +25,7 @@
package sonia.scm.security.gpg;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -34,8 +35,8 @@ import sonia.scm.security.PublicKey;
import java.io.IOException;
import java.time.Instant;
+import java.util.Collections;
import java.util.Optional;
-import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@@ -51,7 +52,7 @@ class DefaultGPGTest {
@Test
void shouldFindIdInSignature() throws IOException {
- String raw = GPGTestHelper.readKey("signature.asc");
+ String raw = GPGTestHelper.readResource("signature.asc");
String publicKeyId = gpg.findPublicKeyId(raw.getBytes());
assertThat(publicKeyId).isEqualTo("0x1F17B79A09DAD5B9");
@@ -59,8 +60,8 @@ class DefaultGPGTest {
@Test
void shouldFindPublicKey() throws IOException {
- String raw = GPGTestHelper.readKey("subkeys.asc");
- RawGpgKey key1 = new RawGpgKey("42", "key_42", "trillian", raw, Instant.now());
+ String raw = GPGTestHelper.readResource("subkeys.asc");
+ RawGpgKey key1 = new RawGpgKey("42", "key_42", "trillian", raw, ImmutableSet.of("trillian", "zaphod"), Instant.now());
when(store.findById("42")).thenReturn(Optional.of(key1));
@@ -70,17 +71,16 @@ class DefaultGPGTest {
assertThat(publicKey.get().getOwner()).isPresent();
assertThat(publicKey.get().getOwner().get()).contains("trillian");
assertThat(publicKey.get().getId()).isEqualTo("42");
- assertThat(publicKey.get().getContacts()).contains("Sebastian Sdorra ",
- "Sebastian Sdorra ");
+ assertThat(publicKey.get().getContacts()).contains("trillian", "zaphod");
}
@Test
void shouldFindKeysForUsername() throws IOException {
- String raw = GPGTestHelper.readKey("single.asc");
- String raw2= GPGTestHelper.readKey("subkeys.asc");
+ String raw = GPGTestHelper.readResource("single.asc");
+ String raw2= GPGTestHelper.readResource("subkeys.asc");
- RawGpgKey key1 = new RawGpgKey("1", "1", "trillian", raw, Instant.now());
- RawGpgKey key2 = new RawGpgKey("2", "2", "trillian", raw2, Instant.now());
+ RawGpgKey key1 = new RawGpgKey("1", "1", "trillian", raw, Collections.emptySet(), Instant.now());
+ RawGpgKey key2 = new RawGpgKey("2", "2", "trillian", raw2, Collections.emptySet(), Instant.now());
when(store.findByUsername("trillian")).thenReturn(ImmutableList.of(key1, key2));
Iterable keys = gpg.findPublicKeysByUsername("trillian");
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGTestHelper.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGTestHelper.java
index 4608aace5c..dde8e21d52 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGTestHelper.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGTestHelper.java
@@ -36,8 +36,8 @@ final class GPGTestHelper {
}
@SuppressWarnings("UnstableApiUsage")
- static String readKey(String key) throws IOException {
- URL resource = Resources.getResource("sonia/scm/security/gpg/" + key);
+ static String readResource(String fileName) throws IOException {
+ URL resource = Resources.getResource("sonia/scm/security/gpg/" + fileName);
return Resources.toString(resource, StandardCharsets.UTF_8);
}
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java
index 6fb6a3ac69..3e399ae9be 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java
@@ -27,6 +27,9 @@ package sonia.scm.security.gpg;
import org.junit.jupiter.api.Test;
import java.io.IOException;
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
class GpgKeyTest {
@@ -37,13 +40,14 @@ class GpgKeyTest {
longContent.append(i);
}
- byte[] raw = GPGTestHelper.readKey("subkeys.asc").getBytes();
+ String raw = GPGTestHelper.readResource("pubKeyEH.asc");
+ String signature = GPGTestHelper.readResource("signature.asc");
- GpgKey key = new GpgKey("1", "trillian", raw);
+ GpgKey key = new GpgKey("1", "trillian", raw, Collections.emptySet());
- boolean verified = key.verify(longContent.toString().getBytes(), raw);
+ boolean verified = key.verify(longContent.toString().getBytes(), signature.getBytes());
- // assertThat(verified).isTrue();
+ //assertThat(verified).isTrue();
}
}
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysTest.java
index 3bc97eca2a..07a482cbfb 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysTest.java
@@ -38,21 +38,21 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-import static sonia.scm.security.gpg.GPGTestHelper.readKey;
+import static sonia.scm.security.gpg.GPGTestHelper.readResource;
@ExtendWith(MockitoExtension.class)
class KeysTest {
@Test
void shouldResolveSingleId() throws IOException {
- String rawPublicKey = readKey("single.asc");
+ String rawPublicKey = readResource("single.asc");
Keys keys = Keys.resolve(rawPublicKey);
assertThat(keys.getMaster()).isEqualTo("0x975922F193B07D6E");
}
@Test
void shouldResolveIdsFromSubkeys() throws IOException {
- String rawPublicKey = readKey("subkeys.asc");
+ String rawPublicKey = readResource("subkeys.asc");
Keys keys = Keys.resolve(rawPublicKey);
assertThat(keys.getMaster()).isEqualTo("0x13B13D4C8A9350A1");
assertThat(keys.getSubs()).containsOnly("0x247E908C6FD35473", "0xE50E1DD8B90D3A6B", "0xBF49759E43DD0E60");
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PgpPublicKeyExtractorTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PgpPublicKeyExtractorTest.java
new file mode 100644
index 0000000000..a49f5cfb6b
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PgpPublicKeyExtractorTest.java
@@ -0,0 +1,47 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security.gpg;
+
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class PgpPublicKeyExtractorTest {
+
+ @Test
+ void shouldExtractPublicKeyFromRawKey() throws IOException {
+ String raw = GPGTestHelper.readResource("pubKeyEH.asc");
+
+ Optional publicKey = PgpPublicKeyExtractor.getFromRawKey(raw);
+
+ assertThat(publicKey).isPresent();
+ assertThat(Long.toHexString(publicKey.get().getKeyID())).isEqualTo("39ad4bed55527f1c");
+ }
+
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyCollectionMapperTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyCollectionMapperTest.java
index 6bd69efdc1..b82261c4ad 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyCollectionMapperTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyCollectionMapperTest.java
@@ -40,6 +40,7 @@ import sonia.scm.api.v2.resources.ScmPathInfoStore;
import java.io.IOException;
import java.net.URI;
import java.time.Instant;
+import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@@ -103,8 +104,8 @@ class PublicKeyCollectionMapperTest {
}
private RawGpgKey createPublicKey(String displayName) throws IOException {
- String raw = GPGTestHelper.readKey("single.asc");
- return new RawGpgKey(displayName, displayName, "trillian", raw, Instant.now());
+ String raw = GPGTestHelper.readResource("single.asc");
+ return new RawGpgKey(displayName, displayName, "trillian", raw, Collections.emptySet(), Instant.now());
}
}
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyMapperTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyMapperTest.java
index 0e7bd496c8..90af5bd984 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyMapperTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyMapperTest.java
@@ -38,6 +38,7 @@ import sonia.scm.api.v2.resources.ScmPathInfoStore;
import java.io.IOException;
import java.net.URI;
import java.time.Instant;
+import java.util.Collections;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@@ -68,8 +69,8 @@ class PublicKeyMapperTest {
void shouldMapKeyToDto() throws IOException {
when(subject.isPermitted("user:changePublicKeys:trillian")).thenReturn(true);
- String raw = GPGTestHelper.readKey("single.asc");
- RawGpgKey key = new RawGpgKey("1", "key_42", "trillian", raw, Instant.now());
+ String raw = GPGTestHelper.readResource("single.asc");
+ RawGpgKey key = new RawGpgKey("1", "key_42", "trillian", raw, Collections.emptySet(), Instant.now());
RawGpgKeyDto dto = mapper.map(key);
@@ -82,8 +83,8 @@ class PublicKeyMapperTest {
@Test
void shouldNotAppendDeleteLink() throws IOException {
- String raw = GPGTestHelper.readKey("single.asc");
- RawGpgKey key = new RawGpgKey("1", "key_42", "trillian", raw, Instant.now());
+ String raw = GPGTestHelper.readResource("single.asc");
+ RawGpgKey key = new RawGpgKey("1", "key_42", "trillian", raw, Collections.emptySet(), Instant.now());
RawGpgKeyDto dto = mapper.map(key);
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyResourceTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyResourceTest.java
index 112fc1b31f..45cf1b0fa3 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyResourceTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyResourceTest.java
@@ -112,7 +112,7 @@ class PublicKeyResourceTest {
@Test
void shouldAddToStore() throws URISyntaxException, IOException {
- String raw = GPGTestHelper.readKey("single.asc");
+ String raw = GPGTestHelper.readResource("single.asc");
UriInfo uriInfo = mock(UriInfo.class);
UriBuilder builder = mock(UriBuilder.class);
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java
index 1871827939..b57cf1430d 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java
@@ -80,21 +80,21 @@ class PublicKeyStoreTest {
@Test
void shouldThrowAuthorizationExceptionOnAdd() throws IOException {
doThrow(AuthorizationException.class).when(subject).checkPermission("user:changePublicKeys:zaphod");
- String rawKey = GPGTestHelper.readKey("single.asc");
+ String rawKey = GPGTestHelper.readResource("single.asc");
assertThrows(AuthorizationException.class, () -> keyStore.add("zaphods key", "zaphod", rawKey));
}
@Test
void shouldOnlyStorePublicKeys() throws IOException {
- String rawKey = GPGTestHelper.readKey("single.asc").replace("PUBLIC", "PRIVATE");
+ String rawKey = GPGTestHelper.readResource("single.asc").replace("PUBLIC", "PRIVATE");
assertThrows(NotPublicKeyException.class, () -> keyStore.add("SCM Package Key", "trillian", rawKey));
}
@Test
void shouldReturnStoredKey() throws IOException {
- String rawKey = GPGTestHelper.readKey("single.asc");
+ String rawKey = GPGTestHelper.readResource("single.asc");
Instant now = Instant.now();
RawGpgKey key = keyStore.add("SCM Package Key", "trillian", rawKey);
@@ -107,7 +107,7 @@ class PublicKeyStoreTest {
@Test
void shouldFindStoredKeyById() throws IOException {
- String rawKey = GPGTestHelper.readKey("single.asc");
+ String rawKey = GPGTestHelper.readResource("single.asc");
keyStore.add("SCM Package Key", "trillian", rawKey);
Optional key = keyStore.findById("0x975922F193B07D6E");
assertThat(key).isPresent();
@@ -115,7 +115,7 @@ class PublicKeyStoreTest {
@Test
void shouldDeleteKey() throws IOException {
- String rawKey = GPGTestHelper.readKey("single.asc");
+ String rawKey = GPGTestHelper.readResource("single.asc");
keyStore.add("SCM Package Key", "trillian", rawKey);
Optional key = keyStore.findById("0x975922F193B07D6E");
@@ -139,10 +139,10 @@ class PublicKeyStoreTest {
@Test
void shouldFindAllKeysForUser() throws IOException {
- String singleKey = GPGTestHelper.readKey("single.asc");
+ String singleKey = GPGTestHelper.readResource("single.asc");
keyStore.add("SCM Single Key", "trillian", singleKey);
- String multiKey = GPGTestHelper.readKey("subkeys.asc");
+ String multiKey = GPGTestHelper.readResource("subkeys.asc");
keyStore.add("SCM Multi Key", "trillian", multiKey);
List keys = keyStore.findByUsername("trillian");
diff --git a/scm-webapp/src/test/resources/sonia/scm/security/gpg/pubKeyEH.asc b/scm-webapp/src/test/resources/sonia/scm/security/gpg/pubKeyEH.asc
new file mode 100644
index 0000000000..d8cfe4c4e2
--- /dev/null
+++ b/scm-webapp/src/test/resources/sonia/scm/security/gpg/pubKeyEH.asc
@@ -0,0 +1,109 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+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
+=6sgk
+-----END PGP PUBLIC KEY BLOCK-----
From a1153df50a96642b0b1305ed2d2c15c4669dd141 Mon Sep 17 00:00:00 2001
From: Eduard Heimbuch
Date: Thu, 30 Jul 2020 09:37:43 +0200
Subject: [PATCH 13/46] refactor
---
scm-ui/ui-types/src/index.ts | 2 +-
scm-ui/ui-webapp/public/locales/de/repos.json | 1 +
scm-ui/ui-webapp/public/locales/en/repos.json | 1 +
.../components/changesets/SignatureIcon.tsx | 13 ++++-----
.../publicKeys/formatPublicKey.test.ts | 27 +++++--------------
.../components/publicKeys/formatPublicKey.ts | 5 +++-
6 files changed, 20 insertions(+), 29 deletions(-)
diff --git a/scm-ui/ui-types/src/index.ts b/scm-ui/ui-types/src/index.ts
index a0ce96dac9..7a36ecfc3e 100644
--- a/scm-ui/ui-types/src/index.ts
+++ b/scm-ui/ui-types/src/index.ts
@@ -36,7 +36,7 @@ export { Branch, BranchRequest } from "./Branches";
export { Person } from "./Person";
-export { Changeset, Contributor, ParentChangeset } from "./Changesets";
+export { Changeset, Contributor, ParentChangeset, Signature } from "./Changesets";
export { AnnotatedSource, AnnotatedLine } from "./Annotate";
diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json
index 71eb6a9f98..6d34e782c6 100644
--- a/scm-ui/ui-webapp/public/locales/de/repos.json
+++ b/scm-ui/ui-webapp/public/locales/de/repos.json
@@ -92,6 +92,7 @@
"signatureStatus": "Status",
"keyId": "Schlüssel-ID",
"keyContacts": "Kontakte",
+ "noOwner": "Unbekannt",
"signatureVerified": "Verifiziert",
"signatureNotVerified": "Nicht verifiziert",
"signatureInvalid": "Ungültig",
diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json
index a53146bc78..63c14a73db 100644
--- a/scm-ui/ui-webapp/public/locales/en/repos.json
+++ b/scm-ui/ui-webapp/public/locales/en/repos.json
@@ -90,6 +90,7 @@
"signedBy": "Signed by",
"keyId": "Key ID",
"keyContacts": "Contacts",
+ "noOwner": "Unknown",
"signatureStatus": "Status",
"signatureVerified": "verified",
"signatureNotVerified": "not verified",
diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/SignatureIcon.tsx b/scm-ui/ui-webapp/src/repos/components/changesets/SignatureIcon.tsx
index bd3b4863d8..1167b50b66 100644
--- a/scm-ui/ui-webapp/src/repos/components/changesets/SignatureIcon.tsx
+++ b/scm-ui/ui-webapp/src/repos/components/changesets/SignatureIcon.tsx
@@ -24,10 +24,10 @@
import React, { FC } from "react";
import { Icon, Tooltip } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
-//import { Signature } from "@scm-manager/ui-types";
+import { Signature } from "@scm-manager/ui-types";
type Props = {
- signatures: any[];
+ signatures: Signature[];
className: any;
};
@@ -36,6 +36,10 @@ const SignatureIcon: FC = ({ signatures, className }) => {
const signature = signatures?.length > 0 ? signatures[0] : undefined;
+ if (!signature) {
+ return null;
+ }
+
const createTooltipMessage = () => {
let status;
if (signature.status === "VERIFIED") {
@@ -50,7 +54,7 @@ const SignatureIcon: FC = ({ signatures, className }) => {
return `${t("changeset.signatureStatus")}: ${status}`;
}
- let message = `${t("changeset.signedBy")}: ${signature.owner}\n${t("changeset.keyId")}: ${signature.keyId}\n${t(
+ let message = `${t("changeset.signedBy")}: ${signature.owner ? signature.owner : t("changeset.noOwner")}\n${t("changeset.keyId")}: ${signature.keyId}\n${t(
"changeset.signatureStatus"
)}: ${status}`;
@@ -71,9 +75,6 @@ const SignatureIcon: FC = ({ signatures, className }) => {
return undefined;
};
- if (!signature) {
- return null;
- }
return (
diff --git a/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.test.ts b/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.test.ts
index e2377d10b4..ddc1ba076b 100644
--- a/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.test.ts
+++ b/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.test.ts
@@ -23,29 +23,14 @@
*/
import { formatPublicKey } from "./formatPublicKey";
-describe("format authorized key tests", () => {
+describe("format public key tests", () => {
it("should format the given key", () => {
- const key = "ssh-rsa ACB0DEFGHIJKLMOPQRSTUVWXYZ tricia@hitchhiker.com";
- expect(formatPublicKey(key)).toEqual("ssh-rsa ... tricia@hitchhiker.com");
+ const key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nA1B2C3D4E5F6HDJSURNSKFHSNEKFHK443MKD\n-----END PGP PUBLIC KEY BLOCK-----";
+ expect(formatPublicKey(key)).toEqual("A1B2C3D4E5F6HDJ");
});
- it("should use the first chars of the key without prefix", () => {
- const key = "ACB0DEFGHIJKLMOPQRSTUVWXYZ tricia@hitchhiker.com";
- expect(formatPublicKey(key)).toEqual("ACB0DEF... tricia@hitchhiker.com");
- });
-
- it("should use the last chars of the key without suffix", () => {
- const key = "ssh-rsa ACB0DEFGHIJKLMOPQRSTUVWXYZ";
- expect(formatPublicKey(key)).toEqual("ssh-rsa ...TUVWXYZ");
- });
-
- it("should use a few chars from the beginning and a few from the end, if the key has no prefix and suffix", () => {
- const key = "ACB0DEFGHIJKLMOPQRSTUVWXYZ0123456789";
- expect(formatPublicKey(key)).toEqual("ACB0DEF...3456789");
- });
-
- it("should return the whole string for a short key", () => {
- const key = "ABCDE";
- expect(formatPublicKey(key)).toEqual("ABCDE");
+ it("should format bad formatted key", () => {
+ const key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n\nA1B2C3D4E5F6HDJSURNSKFHSNEKFHK443MKD\n\n\n-----END PGP PUBLIC KEY BLOCK-----";
+ expect(formatPublicKey(key)).toEqual("-----BEGIN PGP ");
});
});
diff --git a/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.ts b/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.ts
index af17e1b362..322ebb88e8 100644
--- a/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.ts
+++ b/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.ts
@@ -24,5 +24,8 @@
export const formatPublicKey = (key: string) => {
const parts = key.split(/\n/);
- return parts[2].substring(0, 15);
+ if (parts[2].length >= 15) {
+ return parts[2].substring(0, 15);
+ }
+ return parts[0].substring(0, 15);
};
From 274ce561fe7f739e2e865aa1cea2fbfc49a0082f Mon Sep 17 00:00:00 2001
From: Sebastian Sdorra
Date: Thu, 30 Jul 2020 11:58:11 +0200
Subject: [PATCH 14/46] fixes gpg verification
---
.../java/sonia/scm/security/gpg/GpgKey.java | 80 ++++++++-----
.../scm/security/gpg/PublicKeyStore.java | 2 -
.../scm/security/gpg/DefaultGPGTest.java | 8 +-
.../sonia/scm/security/gpg/GPGTestHelper.java | 8 +-
.../sonia/scm/security/gpg/GpgKeyTest.java | 17 +--
.../java/sonia/scm/security/gpg/KeysTest.java | 6 +-
.../gpg/PgpPublicKeyExtractorTest.java | 2 +-
.../gpg/PublicKeyCollectionMapperTest.java | 2 +-
.../scm/security/gpg/PublicKeyMapperTest.java | 4 +-
.../security/gpg/PublicKeyResourceTest.java | 2 +-
.../scm/security/gpg/PublicKeyStoreTest.java | 14 +--
.../sonia/scm/security/gpg/pubKeyEH.asc | 109 ------------------
.../sonia/scm/security/gpg/signature.asc | 16 ---
.../sonia/scm/security/gpg/slarti.txt | 9 ++
.../sonia/scm/security/gpg/slarti.txt.asc | 16 +++
15 files changed, 108 insertions(+), 187 deletions(-)
delete mode 100644 scm-webapp/src/test/resources/sonia/scm/security/gpg/pubKeyEH.asc
delete mode 100644 scm-webapp/src/test/resources/sonia/scm/security/gpg/signature.asc
create mode 100644 scm-webapp/src/test/resources/sonia/scm/security/gpg/slarti.txt
create mode 100644 scm-webapp/src/test/resources/sonia/scm/security/gpg/slarti.txt.asc
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java
index 2855a0d265..e2552e1e39 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java
@@ -24,29 +24,27 @@
package sonia.scm.security.gpg;
-import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
-import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider;
+import org.bouncycastle.openpgp.PGPUtil;
+import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.security.PublicKey;
-import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.Set;
-import static sonia.scm.security.gpg.PgpPublicKeyExtractor.getFromRawKey;
-
public class GpgKey implements PublicKey {
private static final Logger LOG = LoggerFactory.getLogger(GpgKey.class);
@@ -90,35 +88,59 @@ public class GpgKey implements PublicKey {
public boolean verify(InputStream stream, byte[] signature) {
boolean verified = false;
try {
- ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(signature));
- PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(armoredInputStream, null);
- PGPSignature pgpSignature = ((PGPSignatureList) pgpObjectFactory.nextObject()).get(0);
-
- PGPContentVerifierBuilderProvider provider = new JcaPGPContentVerifierBuilderProvider();
-
- Optional pgpPublicKey = getFromRawKey(raw);
-
- if (pgpPublicKey.isPresent()) {
- pgpSignature.init(provider, pgpPublicKey.get());
-
- char[] buffer = new char[1024];
- int bytesRead = 0;
- BufferedReader in = new BufferedReader(new InputStreamReader(stream));
-
- while (bytesRead != -1) {
- bytesRead = in.read(buffer, 0, 1024);
- pgpSignature.update(new String(buffer).getBytes(StandardCharsets.UTF_8));
- }
-
- verified = pgpSignature.verify();
- }
-
+ verified = verify(stream, asDecodedStream(signature));
} catch (IOException | PGPException e) {
LOG.error("Could not verify GPG key", e);
}
return verified;
}
+
+ private boolean verify(InputStream stream, InputStream signature) throws IOException, PGPException {
+ PGPObjectFactory pgpObjectFactory = new JcaPGPObjectFactory(signature);
+ Object o = pgpObjectFactory.nextObject();
+ if (o instanceof PGPSignatureList) {
+ return verify(stream, ((PGPSignatureList) o).get(0));
+ } else if (o instanceof PGPCompressedData) {
+ return verify(stream, ((PGPCompressedData) o).getDataStream());
+ } else {
+ LOG.warn("could not find valid signature, only found {}", o);
+ return false;
+ }
+ }
+
+ private boolean verify(InputStream stream, PGPSignature signature) throws IOException, PGPException {
+ PGPPublicKey publicKey = findKey(signature);
+ if (publicKey != null) {
+ JcaPGPContentVerifierBuilderProvider provider = new JcaPGPContentVerifierBuilderProvider();
+ signature.init(provider, publicKey);
+
+ int bytesRead;
+ byte[] buffer = new byte[1024];
+ while ((bytesRead = stream.read(buffer, 0, buffer.length)) != -1) {
+ signature.update(buffer, 0, bytesRead);
+ }
+
+ return signature.verify();
+ } else {
+ LOG.warn("failed to parse public gpg key");
+ }
+ return false;
+ }
+
+ private PGPPublicKey findKey(PGPSignature signature) throws IOException {
+ PGPObjectFactory pgpObjectFactory = new JcaPGPObjectFactory(asDecodedStream(raw));
+ PGPPublicKeyRing keyRing = (PGPPublicKeyRing) pgpObjectFactory.nextObject();
+ return keyRing.getPublicKey(signature.getKeyID());
+ }
+
+ private InputStream asDecodedStream(String content) throws IOException {
+ return asDecodedStream(content.getBytes(StandardCharsets.US_ASCII));
+ }
+
+ private InputStream asDecodedStream(byte[] bytes) throws IOException {
+ return PGPUtil.getDecoderStream(new ByteArrayInputStream(bytes));
+ }
}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java
index 7308ea6cfe..cb606af1ca 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java
@@ -49,8 +49,6 @@ import static sonia.scm.security.gpg.PgpPublicKeyExtractor.getFromRawKey;
@Singleton
public class PublicKeyStore {
- private static final Logger LOG = LoggerFactory.getLogger(PublicKeyStore.class);
-
private static final String STORE_NAME = "gpg_public_keys";
private static final String SUBKEY_STORE_NAME = "gpg_public_sub_keys";
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultGPGTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultGPGTest.java
index d98aa56b85..a3b4b46582 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultGPGTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultGPGTest.java
@@ -52,7 +52,7 @@ class DefaultGPGTest {
@Test
void shouldFindIdInSignature() throws IOException {
- String raw = GPGTestHelper.readResource("signature.asc");
+ String raw = GPGTestHelper.readResourceAsString("signature.asc");
String publicKeyId = gpg.findPublicKeyId(raw.getBytes());
assertThat(publicKeyId).isEqualTo("0x1F17B79A09DAD5B9");
@@ -60,7 +60,7 @@ class DefaultGPGTest {
@Test
void shouldFindPublicKey() throws IOException {
- String raw = GPGTestHelper.readResource("subkeys.asc");
+ String raw = GPGTestHelper.readResourceAsString("subkeys.asc");
RawGpgKey key1 = new RawGpgKey("42", "key_42", "trillian", raw, ImmutableSet.of("trillian", "zaphod"), Instant.now());
when(store.findById("42")).thenReturn(Optional.of(key1));
@@ -76,8 +76,8 @@ class DefaultGPGTest {
@Test
void shouldFindKeysForUsername() throws IOException {
- String raw = GPGTestHelper.readResource("single.asc");
- String raw2= GPGTestHelper.readResource("subkeys.asc");
+ String raw = GPGTestHelper.readResourceAsString("single.asc");
+ String raw2= GPGTestHelper.readResourceAsString("subkeys.asc");
RawGpgKey key1 = new RawGpgKey("1", "1", "trillian", raw, Collections.emptySet(), Instant.now());
RawGpgKey key2 = new RawGpgKey("2", "2", "trillian", raw2, Collections.emptySet(), Instant.now());
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGTestHelper.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGTestHelper.java
index dde8e21d52..b295139a65 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGTestHelper.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGTestHelper.java
@@ -36,7 +36,13 @@ final class GPGTestHelper {
}
@SuppressWarnings("UnstableApiUsage")
- static String readResource(String fileName) throws IOException {
+ static byte[] readResourceAsBytes(String fileName) throws IOException {
+ URL resource = Resources.getResource("sonia/scm/security/gpg/" + fileName);
+ return Resources.toByteArray(resource);
+ }
+
+ @SuppressWarnings("UnstableApiUsage")
+ static String readResourceAsString(String fileName) throws IOException {
URL resource = Resources.getResource("sonia/scm/security/gpg/" + fileName);
return Resources.toString(resource, StandardCharsets.UTF_8);
}
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java
index 3e399ae9be..a830784988 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java
@@ -35,19 +35,14 @@ class GpgKeyTest {
@Test
void shouldVerifyPublicKey() throws IOException {
- StringBuilder longContent = new StringBuilder();
- for (int i = 1; i < 10000; i++) {
- longContent.append(i);
- }
+ String rawPublicKey = GPGTestHelper.readResourceAsString("subkeys.asc");
+ GpgKey publicKey = new GpgKey("1", "trillian", rawPublicKey, Collections.emptySet());
- String raw = GPGTestHelper.readResource("pubKeyEH.asc");
- String signature = GPGTestHelper.readResource("signature.asc");
+ byte[] content = GPGTestHelper.readResourceAsBytes("slarti.txt");
+ byte[] signature = GPGTestHelper.readResourceAsBytes("slarti.txt.asc");
- GpgKey key = new GpgKey("1", "trillian", raw, Collections.emptySet());
-
- boolean verified = key.verify(longContent.toString().getBytes(), signature.getBytes());
-
- //assertThat(verified).isTrue();
+ boolean verified = publicKey.verify(content, signature);
+ assertThat(verified).isTrue();
}
}
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysTest.java
index 07a482cbfb..b78a0c5795 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysTest.java
@@ -38,21 +38,21 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-import static sonia.scm.security.gpg.GPGTestHelper.readResource;
+import static sonia.scm.security.gpg.GPGTestHelper.readResourceAsString;
@ExtendWith(MockitoExtension.class)
class KeysTest {
@Test
void shouldResolveSingleId() throws IOException {
- String rawPublicKey = readResource("single.asc");
+ String rawPublicKey = readResourceAsString("single.asc");
Keys keys = Keys.resolve(rawPublicKey);
assertThat(keys.getMaster()).isEqualTo("0x975922F193B07D6E");
}
@Test
void shouldResolveIdsFromSubkeys() throws IOException {
- String rawPublicKey = readResource("subkeys.asc");
+ String rawPublicKey = readResourceAsString("subkeys.asc");
Keys keys = Keys.resolve(rawPublicKey);
assertThat(keys.getMaster()).isEqualTo("0x13B13D4C8A9350A1");
assertThat(keys.getSubs()).containsOnly("0x247E908C6FD35473", "0xE50E1DD8B90D3A6B", "0xBF49759E43DD0E60");
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PgpPublicKeyExtractorTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PgpPublicKeyExtractorTest.java
index a49f5cfb6b..ca50d58367 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/PgpPublicKeyExtractorTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PgpPublicKeyExtractorTest.java
@@ -36,7 +36,7 @@ class PgpPublicKeyExtractorTest {
@Test
void shouldExtractPublicKeyFromRawKey() throws IOException {
- String raw = GPGTestHelper.readResource("pubKeyEH.asc");
+ String raw = GPGTestHelper.readResourceAsString("pubKeyEH.asc");
Optional publicKey = PgpPublicKeyExtractor.getFromRawKey(raw);
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyCollectionMapperTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyCollectionMapperTest.java
index b82261c4ad..5fe72825c4 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyCollectionMapperTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyCollectionMapperTest.java
@@ -104,7 +104,7 @@ class PublicKeyCollectionMapperTest {
}
private RawGpgKey createPublicKey(String displayName) throws IOException {
- String raw = GPGTestHelper.readResource("single.asc");
+ String raw = GPGTestHelper.readResourceAsString("single.asc");
return new RawGpgKey(displayName, displayName, "trillian", raw, Collections.emptySet(), Instant.now());
}
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyMapperTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyMapperTest.java
index 90af5bd984..13f299e826 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyMapperTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyMapperTest.java
@@ -69,7 +69,7 @@ class PublicKeyMapperTest {
void shouldMapKeyToDto() throws IOException {
when(subject.isPermitted("user:changePublicKeys:trillian")).thenReturn(true);
- String raw = GPGTestHelper.readResource("single.asc");
+ String raw = GPGTestHelper.readResourceAsString("single.asc");
RawGpgKey key = new RawGpgKey("1", "key_42", "trillian", raw, Collections.emptySet(), Instant.now());
RawGpgKeyDto dto = mapper.map(key);
@@ -83,7 +83,7 @@ class PublicKeyMapperTest {
@Test
void shouldNotAppendDeleteLink() throws IOException {
- String raw = GPGTestHelper.readResource("single.asc");
+ String raw = GPGTestHelper.readResourceAsString("single.asc");
RawGpgKey key = new RawGpgKey("1", "key_42", "trillian", raw, Collections.emptySet(), Instant.now());
RawGpgKeyDto dto = mapper.map(key);
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyResourceTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyResourceTest.java
index 45cf1b0fa3..ff11515014 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyResourceTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyResourceTest.java
@@ -112,7 +112,7 @@ class PublicKeyResourceTest {
@Test
void shouldAddToStore() throws URISyntaxException, IOException {
- String raw = GPGTestHelper.readResource("single.asc");
+ String raw = GPGTestHelper.readResourceAsString("single.asc");
UriInfo uriInfo = mock(UriInfo.class);
UriBuilder builder = mock(UriBuilder.class);
diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java
index b57cf1430d..d44f8e2cf7 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java
@@ -80,21 +80,21 @@ class PublicKeyStoreTest {
@Test
void shouldThrowAuthorizationExceptionOnAdd() throws IOException {
doThrow(AuthorizationException.class).when(subject).checkPermission("user:changePublicKeys:zaphod");
- String rawKey = GPGTestHelper.readResource("single.asc");
+ String rawKey = GPGTestHelper.readResourceAsString("single.asc");
assertThrows(AuthorizationException.class, () -> keyStore.add("zaphods key", "zaphod", rawKey));
}
@Test
void shouldOnlyStorePublicKeys() throws IOException {
- String rawKey = GPGTestHelper.readResource("single.asc").replace("PUBLIC", "PRIVATE");
+ String rawKey = GPGTestHelper.readResourceAsString("single.asc").replace("PUBLIC", "PRIVATE");
assertThrows(NotPublicKeyException.class, () -> keyStore.add("SCM Package Key", "trillian", rawKey));
}
@Test
void shouldReturnStoredKey() throws IOException {
- String rawKey = GPGTestHelper.readResource("single.asc");
+ String rawKey = GPGTestHelper.readResourceAsString("single.asc");
Instant now = Instant.now();
RawGpgKey key = keyStore.add("SCM Package Key", "trillian", rawKey);
@@ -107,7 +107,7 @@ class PublicKeyStoreTest {
@Test
void shouldFindStoredKeyById() throws IOException {
- String rawKey = GPGTestHelper.readResource("single.asc");
+ String rawKey = GPGTestHelper.readResourceAsString("single.asc");
keyStore.add("SCM Package Key", "trillian", rawKey);
Optional key = keyStore.findById("0x975922F193B07D6E");
assertThat(key).isPresent();
@@ -115,7 +115,7 @@ class PublicKeyStoreTest {
@Test
void shouldDeleteKey() throws IOException {
- String rawKey = GPGTestHelper.readResource("single.asc");
+ String rawKey = GPGTestHelper.readResourceAsString("single.asc");
keyStore.add("SCM Package Key", "trillian", rawKey);
Optional key = keyStore.findById("0x975922F193B07D6E");
@@ -139,10 +139,10 @@ class PublicKeyStoreTest {
@Test
void shouldFindAllKeysForUser() throws IOException {
- String singleKey = GPGTestHelper.readResource("single.asc");
+ String singleKey = GPGTestHelper.readResourceAsString("single.asc");
keyStore.add("SCM Single Key", "trillian", singleKey);
- String multiKey = GPGTestHelper.readResource("subkeys.asc");
+ String multiKey = GPGTestHelper.readResourceAsString("subkeys.asc");
keyStore.add("SCM Multi Key", "trillian", multiKey);
List keys = keyStore.findByUsername("trillian");
diff --git a/scm-webapp/src/test/resources/sonia/scm/security/gpg/pubKeyEH.asc b/scm-webapp/src/test/resources/sonia/scm/security/gpg/pubKeyEH.asc
deleted file mode 100644
index d8cfe4c4e2..0000000000
--- a/scm-webapp/src/test/resources/sonia/scm/security/gpg/pubKeyEH.asc
+++ /dev/null
@@ -1,109 +0,0 @@
------BEGIN PGP PUBLIC KEY BLOCK-----
-
-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
-=6sgk
------END PGP PUBLIC KEY BLOCK-----
diff --git a/scm-webapp/src/test/resources/sonia/scm/security/gpg/signature.asc b/scm-webapp/src/test/resources/sonia/scm/security/gpg/signature.asc
deleted file mode 100644
index f3e756a16d..0000000000
--- a/scm-webapp/src/test/resources/sonia/scm/security/gpg/signature.asc
+++ /dev/null
@@ -1,16 +0,0 @@
------BEGIN PGP SIGNATURE-----
-
- iQIzBAABCgAdFiEEAibrQDifYryAFPjQHxe3mgna1bkFAl8gSFMACgkQHxe3mgna
- 1bk0Hg/9HaN7aRkRo1FH5xPnswGFAidHG+XQBFYTK3EhR2g6m4iRCij58qIEMQVh
- gV6FTtz6xGB/Oq32e5+gp9dYTp6lTrfm35hjg7uzwiC7OQx3QswZn7GUX/ZmuLk0
- nqz4ryiVxeMWst47JkKAm9PY6GC+UITaL3tptNF//MBPAwEfNnDP7O667BTvnROh
- 9XaSlIdYlbGxs5YzQK8BMB56YdutTDMtHl92oOjDA7r238dScmlNUSw3IQIsNPkz
- 4WZwGM7HVxw7PjbRoMbwXbNEJ87F4SuBqhWM7BjHEUFS21wH8APtVZrNzd2znveq
- oemn/+9pn0LG3Mg/2FASqHA6X+HS79YT9fb0O3HUvHzlaJmev88h4JSGcTJZG5+o
- a9LEPW56clJYmCq/ghKAyV+bJfUkIAP9i75p4zi8Il4ACJnf9oVRg6RuOTXK5cnf
- bvSzEtBWlXT1ELV52uo9gwcQMqgkQ89p8pTHcYHD3UhfdsuCfzlTGP/aQ/6OUBGg
- k6AFS1kAwalAp2VEOBIJUXylM60VfdLaLfWpgg4T8mq4WIV3ROXTV0o2XciuF4r0
- 2oXtyc84J7nnGJlJ4HqHBqzMNHF4giqVNhKQzBAQ1OEjePBfkW6RMDcYxkLilUrU
- LzguRD1ybuS9xYQK60s2S68sNoAFkY8NTn9z8je1BO7ajzQn/fw=
- =smHD
- -----END PGP SIGNATURE-----
diff --git a/scm-webapp/src/test/resources/sonia/scm/security/gpg/slarti.txt b/scm-webapp/src/test/resources/sonia/scm/security/gpg/slarti.txt
new file mode 100644
index 0000000000..cef9ef5b9f
--- /dev/null
+++ b/scm-webapp/src/test/resources/sonia/scm/security/gpg/slarti.txt
@@ -0,0 +1,9 @@
+Slartibartfast is a Magrathean, and a designer of planets.[2] His favourite part of the job is creating coastlines, the most notable of which are the fjords found on the coast of Norway on planet Earth,[3] for which he won an award. While trapped on prehistoric Earth, Arthur Dent and Ford Prefect see Slartibartfast's signature deep inside a glacier in ancient Norway.
+
+When Earth Mk. II is being made, Slartibartfast is assigned to the continent of Africa. He is unhappy about this because he has begun "doing it with fjords again" (arguing that they give a continent a lovely baroque feel), but has been told by his superiors that they are "not equatorial enough". In relation to this, he expresses the view that he would "far rather be happy than right any day."
+
+In any event, the new Earth is not required and, much to Slartibartfast's disgust, its owners suggested that he take a quick skiing holiday on his glaciers before dismantling them.
+
+Slartibartfast's aircar is later found near the place where Zaphod Beeblebrox, Ford Prefect, Trillian and Arthur Dent are attacked by cops, who are suddenly killed in a way similar to how the cleaning staff in Slartibartfast's study have perished. There is a note pointing to one of the controls in the aircar saying "This is the probably the best button to press."
+
+In Life, the Universe and Everything Slartibartfast has joined the Campaign for Real Time (or "CamTim" as the volunteers casually refer to it, a reference to CAMRA) which tries to preserve events as they happened before time travelling was invented. He picks up Arthur and Ford from Lord's Cricket Ground with his Starship Bistromath, after which they head out to stop the robots of Krikkit from bringing together the pieces of the Wikkit Gate.
diff --git a/scm-webapp/src/test/resources/sonia/scm/security/gpg/slarti.txt.asc b/scm-webapp/src/test/resources/sonia/scm/security/gpg/slarti.txt.asc
new file mode 100644
index 0000000000..f2262dd80d
--- /dev/null
+++ b/scm-webapp/src/test/resources/sonia/scm/security/gpg/slarti.txt.asc
@@ -0,0 +1,16 @@
+-----BEGIN PGP SIGNATURE-----
+
+iQIzBAABCgAdFiEE5uxwNFYM4xFJsy3eJH6QjG/TVHMFAl8ii3QACgkQJH6QjG/T
+VHPIWQ//fz+n5HLeIDWMeMhvkNes8dwGzdfHme/Yyb1vocqGj3VK+xr3YVjum09h
+NjKJvumazdALTUXnXNW9T57LVD3kAJpAnwCHFtIQvPmg0EVn1oz7WDh+YVVA2Ko4
+fGgH0dB64N2FUEmCYU8aV8wKUOgQ8Fh5FcSggzC5UegU9yZou+B38AfI55od1Ay/
+jk5tEExEwsErjjhDZFho/D/Ybp43otj4WtVy+fPHaZYW7TzKRVBi7ngqAlyCFGwO
+W/xEy11nv1apXV+l3iGxJkU2jlCi7ORbxH2ooSyhrC33rWxAtdYxgMElF7lRbnoc
+Pg8EQXZ8zmEwgm9u6+Ng0/qsu/wajV+QKSDMRJMhmFN0zpdvyscvaFcowcu6jW25
+Smz/Gs5B2oASDh/L/sLxUdSfCHVM7gk6HYHWNZgSajtpgLeJy8/wxOSYmB2TD72A
+ktZN2v5adkaHM8rEXLPdD0BtCMGs82pxgHEK42ncW6RFFdiOkgb6KPhkmhlxl0XU
+r64mfHj3n/dNBR5LoSbDFtHD2LakN8CPcubURneA/psfUiUdfktl6KcDYsuS1fJk
++XdxAdVUIqf3MwQU3od1nklu5Sybv5+Q2MZOstGn7opGuQXndKFtnC4WOMfo0w+X
+HTZilw/HDYN0wgzLl5YpHWmZ5MQl5/aN1nn5js3vOhgEF3+qhvQ=
+=3ZJK
+-----END PGP SIGNATURE-----
From 8609a14778c17ef106627ef8a76c1c40fa0dc315 Mon Sep 17 00:00:00 2001
From: Sebastian Sdorra
Date: Thu, 30 Jul 2020 12:27:56 +0200
Subject: [PATCH 15/46] adds signature stories to changeset stories
---
.../repos/changesets/Changesets.stories.tsx | 37 ++++++++++++++++++-
1 file changed, 36 insertions(+), 1 deletion(-)
diff --git a/scm-ui/ui-components/src/repos/changesets/Changesets.stories.tsx b/scm-ui/ui-components/src/repos/changesets/Changesets.stories.tsx
index 2c4f1e48f3..bc0f7d2551 100644
--- a/scm-ui/ui-components/src/repos/changesets/Changesets.stories.tsx
+++ b/scm-ui/ui-components/src/repos/changesets/Changesets.stories.tsx
@@ -37,7 +37,7 @@ import { Changeset } from "@scm-manager/ui-types";
import { Replacement } from "../../SplitAndReplace";
const Wrapper = styled.div`
- margin: 2rem;
+ margin: 4rem;
`;
const robohash = (person: Person) => {
@@ -67,6 +67,10 @@ const withReplacements = (
);
};
+const copy = (input: Object) => {
+ return JSON.parse(JSON.stringify(input));
+}
+
storiesOf("Changesets", module)
.addDecorator(story => {story()})
.addDecorator(storyFn => {storyFn()})
@@ -93,4 +97,35 @@ storiesOf("Changesets", module)
],
five
);
+ })
+ .add("With Valid Signature", () => {
+ const changeset = copy(three);
+ changeset.signatures = [{
+ keyId: "0x247E908C6FD35473",
+ type: "gpg",
+ status: "VERIFIED",
+ owner: "trillian",
+ contacts: ["Tricia Marie McMilla "]
+ }];
+ return ;
+ })
+ .add("With Unkown Signature", () => {
+ const changeset = copy(three);
+ changeset.signatures = [{
+ keyId: "0x247E908C6FD35473",
+ type: "gpg",
+ status: "NOT_FOUND"
+ }];
+ return ;
+ })
+ .add("With Invalid Signature", () => {
+ const changeset = copy(three);
+ changeset.signatures = [{
+ keyId: "0x247E908C6FD35473",
+ type: "gpg",
+ status: "INVALID",
+ owner: "trillian",
+ contacts: ["Tricia Marie McMilla "]
+ }];
+ return ;
});
From 103edf2356dfe1f38d9bc697fb3bf4f5695dd73b Mon Sep 17 00:00:00 2001
From: Sebastian Sdorra
Date: Thu, 30 Jul 2020 12:28:46 +0200
Subject: [PATCH 16/46] fixes some small bugs with signature icon
---
.../src/repos/changesets/ChangesetRow.tsx | 3 +--
.../src/repos}/changesets/SignatureIcon.tsx | 23 +++++++++++++++----
scm-ui/ui-webapp/public/locales/de/repos.json | 2 +-
scm-ui/ui-webapp/public/locales/en/repos.json | 2 +-
.../changesets/ChangesetDetails.tsx | 2 +-
5 files changed, 22 insertions(+), 10 deletions(-)
rename scm-ui/{ui-webapp/src/repos/components => ui-components/src/repos}/changesets/SignatureIcon.tsx (82%)
diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx
index 65b9b2de4c..47823b71fe 100644
--- a/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx
+++ b/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx
@@ -35,8 +35,7 @@ import ChangesetAuthor from "./ChangesetAuthor";
import ChangesetTags from "./ChangesetTags";
import ChangesetButtonGroup from "./ChangesetButtonGroup";
import ChangesetDescription from "./ChangesetDescription";
-import SignatureIcon from "@scm-manager/ui-webapp/src/repos/components/changesets/SignatureIcon";
-import { Level } from "../..";
+import SignatureIcon from "./SignatureIcon";
type Props = WithTranslation & {
repository: Repository;
diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/SignatureIcon.tsx b/scm-ui/ui-components/src/repos/changesets/SignatureIcon.tsx
similarity index 82%
rename from scm-ui/ui-webapp/src/repos/components/changesets/SignatureIcon.tsx
rename to scm-ui/ui-components/src/repos/changesets/SignatureIcon.tsx
index 1167b50b66..57ce8fba38 100644
--- a/scm-ui/ui-webapp/src/repos/components/changesets/SignatureIcon.tsx
+++ b/scm-ui/ui-components/src/repos/changesets/SignatureIcon.tsx
@@ -22,15 +22,26 @@
* SOFTWARE.
*/
import React, { FC } from "react";
-import { Icon, Tooltip } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
import { Signature } from "@scm-manager/ui-types";
+import styled from "styled-components";
+import Icon from "../../Icon";
+import Tooltip from "../../Tooltip";
type Props = {
signatures: Signature[];
className: any;
};
+const StyledIcon = styled(Icon)`
+ width: 1em;
+ height: 1em;
+ vertical-align: middle;
+ border-radius: 0.25em;
+ margin-bottom: 0.2em;
+`;
+
+
const SignatureIcon: FC = ({ signatures, className }) => {
const [t] = useTranslation("repos");
@@ -54,14 +65,16 @@ const SignatureIcon: FC = ({ signatures, className }) => {
return `${t("changeset.signatureStatus")}: ${status}`;
}
- let message = `${t("changeset.signedBy")}: ${signature.owner ? signature.owner : t("changeset.noOwner")}\n${t("changeset.keyId")}: ${signature.keyId}\n${t(
+ let message = `${t("changeset.keyOwner")}: ${signature.owner ? signature.owner : t("changeset.noOwner")}\n${t("changeset.keyId")}: ${signature.keyId}\n${t(
"changeset.signatureStatus"
)}: ${status}`;
if (signature.contacts?.length > 0) {
- message = message + `\n${t("changeset.keyContacts")}: ${signature.contacts.map((contact: string) => `\n- ${contact}`)}`;
+ message += `\n${t("changeset.keyContacts")}:`;
+ signature.contacts.forEach((contact) => {
+ message += `\n- ${contact}`;
+ });
}
-
return message;
};
@@ -78,7 +91,7 @@ const SignatureIcon: FC = ({ signatures, className }) => {
return (
-
+
);
};
diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json
index 6d34e782c6..ad35ed7371 100644
--- a/scm-ui/ui-webapp/public/locales/de/repos.json
+++ b/scm-ui/ui-webapp/public/locales/de/repos.json
@@ -88,7 +88,7 @@
"shortSummary": "Committet <0/> <1/>",
"tags": "Tags",
"diffNotSupported": "Diff des Changesets wird von diesem Repositorytyp nicht unterstützt",
- "signedBy": "Signiert von",
+ "keyOwner": "Schlüssel Besitzer",
"signatureStatus": "Status",
"keyId": "Schlüssel-ID",
"keyContacts": "Kontakte",
diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json
index 63c14a73db..daa5bc0caa 100644
--- a/scm-ui/ui-webapp/public/locales/en/repos.json
+++ b/scm-ui/ui-webapp/public/locales/en/repos.json
@@ -87,7 +87,7 @@
"summary": "Changeset <0/> was committed <1/>",
"shortSummary": "Committed <0/> <1/>",
"tags": "Tags",
- "signedBy": "Signed by",
+ "keyOwner": "Key Owner",
"keyId": "Key ID",
"keyContacts": "Contacts",
"noOwner": "Unknown",
diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx b/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx
index c9bc1c6b6b..c52f8d83a3 100644
--- a/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx
+++ b/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx
@@ -43,7 +43,7 @@ import {
} from "@scm-manager/ui-components";
import ContributorTable from "./ContributorTable";
import { Link as ReactLink } from "react-router-dom";
-import SignatureIcon from "./SignatureIcon";
+import SignatureIcon from "@scm-manager/ui-components/src/repos/changesets/SignatureIcon";
type Props = WithTranslation & {
changeset: Changeset;
From 2763e6eccf6326a4a1484fc700b1ef07c1626ad1 Mon Sep 17 00:00:00 2001
From: Sebastian Sdorra
Date: Thu, 30 Jul 2020 13:27:40 +0200
Subject: [PATCH 17/46] uses id of public key in overview
---
.../components/publicKeys/PublicKeyEntry.tsx | 2 +-
.../components/publicKeys/SetPublicKeys.tsx | 1 +
.../publicKeys/formatPublicKey.test.ts | 36 -------------------
.../components/publicKeys/formatPublicKey.ts | 31 ----------------
.../sonia/scm/security/gpg/RawGpgKeyDto.java | 1 +
5 files changed, 3 insertions(+), 68 deletions(-)
delete mode 100644 scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.test.ts
delete mode 100644 scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.ts
diff --git a/scm-ui/ui-webapp/src/users/components/publicKeys/PublicKeyEntry.tsx b/scm-ui/ui-webapp/src/users/components/publicKeys/PublicKeyEntry.tsx
index 88c1dfe19e..cea7514ab3 100644
--- a/scm-ui/ui-webapp/src/users/components/publicKeys/PublicKeyEntry.tsx
+++ b/scm-ui/ui-webapp/src/users/components/publicKeys/PublicKeyEntry.tsx
@@ -51,7 +51,7 @@ export const PublicKeyEntry: FC = ({ publicKey, onDelete }) => {
-
{formatPublicKey(publicKey.raw)}
+
{publicKey.id}
{deleteButton}
>
diff --git a/scm-ui/ui-webapp/src/users/components/publicKeys/SetPublicKeys.tsx b/scm-ui/ui-webapp/src/users/components/publicKeys/SetPublicKeys.tsx
index a3c141d35a..c975a83533 100644
--- a/scm-ui/ui-webapp/src/users/components/publicKeys/SetPublicKeys.tsx
+++ b/scm-ui/ui-webapp/src/users/components/publicKeys/SetPublicKeys.tsx
@@ -35,6 +35,7 @@ export type PublicKeysCollection = Collection & {
};
export type PublicKey = {
+ id: string;
displayName: string;
raw: string;
created?: string;
diff --git a/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.test.ts b/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.test.ts
deleted file mode 100644
index ddc1ba076b..0000000000
--- a/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.test.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2020-present Cloudogu GmbH and Contributors
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-import { formatPublicKey } from "./formatPublicKey";
-
-describe("format public key tests", () => {
- it("should format the given key", () => {
- const key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nA1B2C3D4E5F6HDJSURNSKFHSNEKFHK443MKD\n-----END PGP PUBLIC KEY BLOCK-----";
- expect(formatPublicKey(key)).toEqual("A1B2C3D4E5F6HDJ");
- });
-
- it("should format bad formatted key", () => {
- const key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n\nA1B2C3D4E5F6HDJSURNSKFHSNEKFHK443MKD\n\n\n-----END PGP PUBLIC KEY BLOCK-----";
- expect(formatPublicKey(key)).toEqual("-----BEGIN PGP ");
- });
-});
diff --git a/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.ts b/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.ts
deleted file mode 100644
index 322ebb88e8..0000000000
--- a/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2020-present Cloudogu GmbH and Contributors
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-export const formatPublicKey = (key: string) => {
- const parts = key.split(/\n/);
- if (parts[2].length >= 15) {
- return parts[2].substring(0, 15);
- }
- return parts[0].substring(0, 15);
-};
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKeyDto.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKeyDto.java
index 2e40555e42..b56df8f726 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKeyDto.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKeyDto.java
@@ -38,6 +38,7 @@ import java.time.Instant;
@SuppressWarnings("squid:S2160") // we do not need equals for dto
public class RawGpgKeyDto extends HalRepresentation {
+ private String id;
private String displayName;
private String raw;
private Instant created;
From 4112a0b70ae314a9ad1a9dde86e093093398bd56 Mon Sep 17 00:00:00 2001
From: Sebastian Sdorra
Date: Thu, 30 Jul 2020 13:28:08 +0200
Subject: [PATCH 18/46] adds public key link to footer
---
scm-ui/ui-components/src/layout/Footer.tsx | 1 +
scm-ui/ui-webapp/public/locales/de/commons.json | 1 +
scm-ui/ui-webapp/public/locales/en/commons.json | 1 +
3 files changed, 3 insertions(+)
diff --git a/scm-ui/ui-components/src/layout/Footer.tsx b/scm-ui/ui-components/src/layout/Footer.tsx
index 355a74c67e..6c103b8f2e 100644
--- a/scm-ui/ui-components/src/layout/Footer.tsx
+++ b/scm-ui/ui-components/src/layout/Footer.tsx
@@ -96,6 +96,7 @@ const Footer: FC = ({ me, version, links }) => {
+ }>
diff --git a/scm-ui/ui-webapp/public/locales/de/commons.json b/scm-ui/ui-webapp/public/locales/de/commons.json
index 158d5e6420..2d410aa9de 100644
--- a/scm-ui/ui-webapp/public/locales/de/commons.json
+++ b/scm-ui/ui-webapp/public/locales/de/commons.json
@@ -69,6 +69,7 @@
"navigationLabel": "Profil",
"informationNavLink": "Information",
"changePasswordNavLink": "Passwort ändern",
+ "publicKeysNavLink": "Öffentliche Schlüssel",
"settingsNavLink": "Einstellungen",
"username": "Benutzername",
"displayName": "Anzeigename",
diff --git a/scm-ui/ui-webapp/public/locales/en/commons.json b/scm-ui/ui-webapp/public/locales/en/commons.json
index 444de0bea5..7439142a23 100644
--- a/scm-ui/ui-webapp/public/locales/en/commons.json
+++ b/scm-ui/ui-webapp/public/locales/en/commons.json
@@ -71,6 +71,7 @@
"informationNavLink": "Information",
"changePasswordNavLink": "Change password",
"settingsNavLink": "Settings",
+ "publicKeysNavLink": "Public Keys",
"username": "Username",
"displayName": "Display Name",
"mail": "E-Mail",
From f4ab367220baab865d23add96e287e25e48d7512 Mon Sep 17 00:00:00 2001
From: Sebastian Sdorra
Date: Thu, 30 Jul 2020 13:35:34 +0200
Subject: [PATCH 19/46] adds key id to the tooltip, which is shown for unknown
keys
---
.../repos/changesets/Changesets.stories.tsx | 22 +++++++++----------
.../src/repos/changesets/SignatureIcon.tsx | 2 +-
2 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/scm-ui/ui-components/src/repos/changesets/Changesets.stories.tsx b/scm-ui/ui-components/src/repos/changesets/Changesets.stories.tsx
index bc0f7d2551..df5960ee52 100644
--- a/scm-ui/ui-components/src/repos/changesets/Changesets.stories.tsx
+++ b/scm-ui/ui-components/src/repos/changesets/Changesets.stories.tsx
@@ -98,7 +98,16 @@ storiesOf("Changesets", module)
five
);
})
- .add("With Valid Signature", () => {
+ .add("With unknown signature", () => {
+ const changeset = copy(three);
+ changeset.signatures = [{
+ keyId: "0x247E908C6FD35473",
+ type: "gpg",
+ status: "NOT_FOUND"
+ }];
+ return ;
+ })
+ .add("With valid signature", () => {
const changeset = copy(three);
changeset.signatures = [{
keyId: "0x247E908C6FD35473",
@@ -109,16 +118,7 @@ storiesOf("Changesets", module)
}];
return ;
})
- .add("With Unkown Signature", () => {
- const changeset = copy(three);
- changeset.signatures = [{
- keyId: "0x247E908C6FD35473",
- type: "gpg",
- status: "NOT_FOUND"
- }];
- return ;
- })
- .add("With Invalid Signature", () => {
+ .add("With invalid signature", () => {
const changeset = copy(three);
changeset.signatures = [{
keyId: "0x247E908C6FD35473",
diff --git a/scm-ui/ui-components/src/repos/changesets/SignatureIcon.tsx b/scm-ui/ui-components/src/repos/changesets/SignatureIcon.tsx
index 57ce8fba38..9f99443be9 100644
--- a/scm-ui/ui-components/src/repos/changesets/SignatureIcon.tsx
+++ b/scm-ui/ui-components/src/repos/changesets/SignatureIcon.tsx
@@ -62,7 +62,7 @@ const SignatureIcon: FC = ({ signatures, className }) => {
}
if (signature.status === "NOT_FOUND") {
- return `${t("changeset.signatureStatus")}: ${status}`;
+ return `${t("changeset.signatureStatus")}: ${status}\n${t("changeset.keyId")}: ${signature.keyId}`;
}
let message = `${t("changeset.keyOwner")}: ${signature.owner ? signature.owner : t("changeset.noOwner")}\n${t("changeset.keyId")}: ${signature.keyId}\n${t(
From 8db0301141182f1cd8dd39e26365ce3e45bd1ccd Mon Sep 17 00:00:00 2001
From: Eduard Heimbuch
Date: Fri, 31 Jul 2020 10:26:44 +0200
Subject: [PATCH 20/46] cleanup
---
.../java/sonia/scm/repository/Signature.java | 2 +-
.../api/RepositoryServiceFactory.java | 6 +
.../java/sonia/scm/security/PublicKey.java | 4 +-
.../scm/security/PublicKeyCreatedEvent.java | 35 ++
.../src/__snapshots__/storyshots.test.ts.snap | 436 +++++++++++++++++-
.../src/repos/changesets/SignatureIcon.tsx | 3 +-
scm-ui/ui-types/src/Changesets.ts | 2 +-
.../java/sonia/scm/security/gpg/GpgKey.java | 7 +-
.../scm/security/gpg/PublicKeyMapper.java | 1 +
.../scm/security/gpg/PublicKeyStore.java | 17 +-
.../sonia/scm/security/gpg/RawGpgKey.java | 3 +-
.../scm/security/gpg/DefaultGPGTest.java | 10 +-
.../gpg/PgpPublicKeyExtractorTest.java | 4 +-
.../scm/security/gpg/PublicKeyMapperTest.java | 1 -
.../scm/security/gpg/PublicKeyStoreTest.java | 5 +
15 files changed, 506 insertions(+), 30 deletions(-)
create mode 100644 scm-core/src/main/java/sonia/scm/security/PublicKeyCreatedEvent.java
diff --git a/scm-core/src/main/java/sonia/scm/repository/Signature.java b/scm-core/src/main/java/sonia/scm/repository/Signature.java
index 0f9a4f996a..f0f3e1e492 100644
--- a/scm-core/src/main/java/sonia/scm/repository/Signature.java
+++ b/scm-core/src/main/java/sonia/scm/repository/Signature.java
@@ -44,7 +44,7 @@ public class Signature implements Serializable {
private final String type;
private final SignatureStatus status;
private final String owner;
- private final Set contacts;
+ private final Set contacts;
public Optional getOwner() {
return Optional.ofNullable(owner);
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java
index fdc936af1f..e954c8088f 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java
@@ -55,6 +55,7 @@ import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.spi.RepositoryServiceProvider;
import sonia.scm.repository.spi.RepositoryServiceResolver;
import sonia.scm.repository.work.WorkdirProvider;
+import sonia.scm.security.PublicKeyCreatedEvent;
import sonia.scm.security.PublicKeyDeletedEvent;
import sonia.scm.security.ScmSecurityException;
@@ -335,6 +336,11 @@ public final class RepositoryServiceFactory {
cacheManager.getCache(LogCommandBuilder.CACHE_NAME).clear();
}
+ @Subscribe
+ public void onEvent(PublicKeyCreatedEvent event) {
+ cacheManager.getCache(LogCommandBuilder.CACHE_NAME).clear();
+ }
+
@SuppressWarnings({"unchecked", "java:S3740", "rawtypes"})
private void clearCaches(final String repositoryId) {
if (logger.isDebugEnabled()) {
diff --git a/scm-core/src/main/java/sonia/scm/security/PublicKey.java b/scm-core/src/main/java/sonia/scm/security/PublicKey.java
index bcce1814fa..003863d696 100644
--- a/scm-core/src/main/java/sonia/scm/security/PublicKey.java
+++ b/scm-core/src/main/java/sonia/scm/security/PublicKey.java
@@ -24,6 +24,8 @@
package sonia.scm.security;
+import sonia.scm.repository.Person;
+
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Optional;
@@ -62,7 +64,7 @@ public interface PublicKey {
*
* @return owner or empty optional
*/
- Set getContacts();
+ Set getContacts();
/**
* Verifies that the signature is valid for the given data.
diff --git a/scm-core/src/main/java/sonia/scm/security/PublicKeyCreatedEvent.java b/scm-core/src/main/java/sonia/scm/security/PublicKeyCreatedEvent.java
new file mode 100644
index 0000000000..a79aeca9c4
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/security/PublicKeyCreatedEvent.java
@@ -0,0 +1,35 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security;
+
+import sonia.scm.event.Event;
+
+/**
+ * This event is fired when a public key was created in SCM-Manager.
+ * @since 2.4.0
+ */
+@Event
+public class PublicKeyCreatedEvent {
+}
diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap
index 69584d643e..957542c72c 100644
--- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap
+++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap
@@ -1716,7 +1716,7 @@ exports[`Storyshots CardColumnSmall Minimal 1`] = `
exports[`Storyshots Changesets Co-Authors with avatar 1`] = `
`;
+exports[`Storyshots Changesets With invalid signature 1`] = `
+