work in progress

This commit is contained in:
Konstantin Schaper
2020-11-24 22:07:24 +01:00
parent 3132dbc0f8
commit 0c5ab90852
17 changed files with 688 additions and 13 deletions

View File

@@ -28,6 +28,8 @@ import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.ToString; import lombok.ToString;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional; import java.util.Optional;
/** /**
@@ -44,6 +46,7 @@ public final class Tag {
private final String name; private final String name;
private final String revision; private final String revision;
private final Long date; private final Long date;
private final List<Signature> signatures = new ArrayList<>();
/** /**
* Constructs a new tag. * Constructs a new tag.
@@ -89,4 +92,8 @@ public final class Tag {
public Optional<Long> getDate() { public Optional<Long> getDate() {
return Optional.ofNullable(date); return Optional.ofNullable(date);
} }
public void addSignature(Signature signature) {
this.signatures.add(signature);
}
} }

View File

@@ -0,0 +1,41 @@
/*
* 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 sonia.scm.event.Event;
/**
* This event is fired when a new tag was created by the SCM-Manager.
* Warning: This event will not be fired if a new tag was pushed.
* @since 2.10.0
*/
@Event
@Value
public class TagCreatedEvent {
Repository repository;
String revision;
String tagName;
}

View File

@@ -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 lombok.Value;
import sonia.scm.event.Event;
/**
* This event is fired when a tag was deleted by the SCM-Manager.
* Warning: This event will not be fired if a tag was removed by a push of a git client.
* @since 2.10.0
*/
@Event
@Value
public class TagDeletedEvent {
Repository repository;
String tagName;
}

View File

@@ -0,0 +1,80 @@
/*
* 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.api;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.Repository;
import sonia.scm.repository.Tag;
import sonia.scm.repository.TagCreatedEvent;
import sonia.scm.repository.TagDeletedEvent;
import sonia.scm.repository.spi.TagCommand;
import java.io.IOException;
public class TagCommandBuilder {
private final Repository repository;
private final TagCommand command;
private final ScmEventBus eventBus;
public TagCommandBuilder(Repository repository, TagCommand command) {
this.repository = repository;
this.command = command;
this.eventBus = ScmEventBus.getInstance();
}
TagCreateCommandBuilder create() {
return new TagCreateCommandBuilder();
}
TagDeleteCommandBuilder delete() {
return new TagDeleteCommandBuilder();
}
private class TagCreateCommandBuilder {
private TagCreateRequest request = new TagCreateRequest();
void setRevision(String revision) {
request.setRevision(revision);
}
void setName(String name) {
request.setName(name);
}
Tag execute() throws IOException {
Tag tag = command.create(request);
eventBus.post(new TagCreatedEvent(repository, request.getRevision(), request.getName()));
return tag;
}
}
private class TagDeleteCommandBuilder {
private TagDeleteRequest request = new TagDeleteRequest();
void execute() throws IOException {
command.delete(request);
eventBus.post(new TagDeletedEvent(repository, request.getName()));
}
}
}

View File

@@ -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.repository.api;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TagCreateRequest {
private String revision;
private String name;
}

View File

@@ -0,0 +1,36 @@
/*
* 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.api;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TagDeleteRequest {
private String name;
}

View File

@@ -0,0 +1,39 @@
/*
* 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.repository.Tag;
import sonia.scm.repository.api.TagDeleteRequest;
import sonia.scm.repository.api.TagCreateRequest;
import java.io.IOException;
/**
* @since 2.11
*/
public interface TagCommand {
Tag create(TagCreateRequest request) throws IOException;
void delete(TagDeleteRequest request) throws IOException;
}

View File

@@ -30,6 +30,7 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
import org.checkerframework.checker.nullness.Opt;
import org.eclipse.jgit.api.FetchCommand; import org.eclipse.jgit.api.FetchCommand;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.GitAPIException;
@@ -52,17 +53,23 @@ import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.LfsFactory; import org.eclipse.jgit.util.LfsFactory;
import org.eclipse.jgit.util.RawParseUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.ContextEntry; import sonia.scm.ContextEntry;
import sonia.scm.security.GPG;
import sonia.scm.security.PublicKey;
import sonia.scm.util.HttpUtil; import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util; import sonia.scm.util.Util;
import sonia.scm.web.GitUserAgentProvider; import sonia.scm.web.GitUserAgentProvider;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -451,6 +458,26 @@ public final class GitUtil
return commit; return commit;
} }
public static RevTag getTag(org.eclipse.jgit.lib.Repository repository,
RevWalk revWalk, Ref ref)
throws IOException
{
RevTag tag = null;
ObjectId id = ref.getObjectId();
if (id != null)
{
if (revWalk == null)
{
revWalk = new RevWalk(repository);
}
tag = revWalk.parseTag(id);
}
return tag;
}
/** /**
* Method description * Method description
* *
@@ -683,6 +710,59 @@ public final class GitUtil
return name; return name;
} }
private static final byte[] GPG_HEADER = {'P', 'G', 'P'};
public static Optional<Signature> getTagSignature(RevObject revObject, GPG gpg) {
if (revObject instanceof RevTag) {
RevTag tag = (RevTag) revObject;
byte[] raw = tag.getFullMessage().getBytes();
int start = RawParseUtils.headerStart(GPG_HEADER, raw, 0);
if (start < 0) {
return Optional.empty();
}
int end = RawParseUtils.headerEnd(raw, start);
byte[] signature = Arrays.copyOfRange(raw, start, end);
String publicKeyId = gpg.findPublicKeyId(signature);
if (Strings.isNullOrEmpty(publicKeyId)) {
// key not found
return Optional.of(new Signature(publicKeyId, "gpg", SignatureStatus.NOT_FOUND, null, Collections.emptySet()));
}
Optional<PublicKey> publicKeyById = gpg.findPublicKey(publicKeyId);
if (!publicKeyById.isPresent()) {
// key not found
return Optional.of(new Signature(publicKeyId, "gpg", SignatureStatus.NOT_FOUND, null, Collections.emptySet()));
}
PublicKey publicKey = publicKeyById.get();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
byte[] headerPrefix = Arrays.copyOfRange(raw, 0, start - GPG_HEADER.length - 1);
baos.write(headerPrefix);
byte[] headerSuffix = Arrays.copyOfRange(raw, end + 1, raw.length);
baos.write(headerSuffix);
} catch (IOException ex) {
// this will never happen, because we are writing into memory
throw new IllegalStateException("failed to write into memory", ex);
}
boolean verified = publicKey.verify(baos.toByteArray(), signature);
return Optional.of(new Signature(
publicKeyId,
"gpg",
verified ? SignatureStatus.VERIFIED : SignatureStatus.INVALID,
publicKey.getOwner().orElse(null),
publicKey.getContacts()
));
}
return Optional.empty();
}
/** /**
* Returns true if the request comes from a git client. * Returns true if the request comes from a git client.
* *

View File

@@ -29,6 +29,7 @@ import com.google.inject.AbstractModule;
import com.google.inject.Injector; import com.google.inject.Injector;
import sonia.scm.repository.Feature; import sonia.scm.repository.Feature;
import sonia.scm.repository.api.Command; import sonia.scm.repository.api.Command;
import sonia.scm.security.GPG;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.Set; import java.util.Set;
@@ -61,12 +62,14 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
protected static final Set<Feature> FEATURES = EnumSet.of(Feature.INCOMING_REVISION); protected static final Set<Feature> FEATURES = EnumSet.of(Feature.INCOMING_REVISION);
private final GitContext context; private final GitContext context;
private final GPG gpg;
private final Injector commandInjector; private final Injector commandInjector;
//~--- constructors --------------------------------------------------------- //~--- constructors ---------------------------------------------------------
GitRepositoryServiceProvider(Injector injector, GitContext context) { GitRepositoryServiceProvider(Injector injector, GitContext context, GPG gpg) {
this.context = context; this.context = context;
this.gpg = gpg;
commandInjector = injector.createChildInjector(new AbstractModule() { commandInjector = injector.createChildInjector(new AbstractModule() {
@Override @Override
protected void configure() { protected void configure() {
@@ -142,7 +145,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
@Override @Override
public TagsCommand getTagsCommand() { public TagsCommand getTagsCommand() {
return new GitTagsCommand(context); return new GitTagsCommand(context, gpg);
} }
@Override @Override

View File

@@ -31,6 +31,7 @@ import com.google.inject.Injector;
import sonia.scm.plugin.Extension; import sonia.scm.plugin.Extension;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.security.GPG;
/** /**
* *
@@ -41,17 +42,19 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver {
private final Injector injector; private final Injector injector;
private final GitContextFactory contextFactory; private final GitContextFactory contextFactory;
private final GPG gpg;
@Inject @Inject
public GitRepositoryServiceResolver(Injector injector, GitContextFactory contextFactory) { public GitRepositoryServiceResolver(Injector injector, GitContextFactory contextFactory, GPG gpg) {
this.injector = injector; this.injector = injector;
this.contextFactory = contextFactory; this.contextFactory = contextFactory;
this.gpg = gpg;
} }
@Override @Override
public GitRepositoryServiceProvider resolve(Repository repository) { public GitRepositoryServiceProvider resolve(Repository repository) {
if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) {
return new GitRepositoryServiceProvider(injector, contextFactory.create(repository)); return new GitRepositoryServiceProvider(injector, contextFactory.create(repository), gpg);
} }
return null; return null;
} }

View File

@@ -0,0 +1,168 @@
/*
* 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.common.base.Strings;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevTag;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.RawParseUtils;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Signature;
import sonia.scm.repository.SignatureStatus;
import sonia.scm.repository.Tag;
import sonia.scm.repository.api.TagDeleteRequest;
import sonia.scm.repository.api.TagCreateRequest;
import sonia.scm.security.GPG;
import sonia.scm.security.PublicKey;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Optional;
public class GitTagCommand extends AbstractGitCommand implements TagCommand {
private final GPG gpg;
GitTagCommand(GitContext context, GPG gpg) {
super(context);
this.gpg = gpg;
}
@Override
public Tag create(TagCreateRequest request) {
try (Git git = new Git(context.open())) {
Tag tag;
String revision = request.getRevision();
RevObject revObject = null;
Long tagTime = null;
if (!Strings.isNullOrEmpty(revision)) {
ObjectId id = git.getRepository().resolve(revision);
try (RevWalk walk = new RevWalk(git.getRepository())) {
revObject = walk.parseAny(id);
tagTime = GitUtil.getTagTime(walk, id);
}
}
Ref ref;
if (revObject != null) {
ref =
git.tag()
.setObjectId(revObject)
.setTagger(new PersonIdent("SCM-Manager", "noreply@scm-manager.org"))
.setName(request.getName())
.call();
} else {
throw new InternalRepositoryException(repository, "could not create tag because revision does not exist");
}
ObjectId objectId;
if (ref.isPeeled()) {
objectId = ref.getPeeledObjectId();
} else {
objectId = ref.getObjectId();
}
tag = new Tag(request.getName(), objectId.toString(), tagTime);
try (RevWalk walk = new RevWalk(git.getRepository())) {
revObject = walk.parseTag(objectId);
tag.addSignature(getTagSignature((RevTag) revObject));
}
return tag;
} catch (IOException | GitAPIException ex) {
throw new InternalRepositoryException(repository, "could not create tag " + request.getName(), ex);
}
}
@Override
public void delete(TagDeleteRequest request) {
try (Git git = new Git(context.open())) {
git.tagDelete().setTags(request.getName()).call();
} catch (GitAPIException | IOException e) {
throw new InternalRepositoryException(repository, "could not delete tag", e);
}
}
private static final byte[] GPG_HEADER = {'g', 'p', 'g', 's', 'i', 'g'};
private Signature getTagSignature(RevTag tag) {
byte[] raw = tag.getFullMessage().getBytes();
int start = RawParseUtils.headerStart(GPG_HEADER, raw, 0);
if (start < 0) {
return null;
}
int end = RawParseUtils.headerEnd(raw, start);
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<PublicKey> publicKeyById = gpg.findPublicKey(publicKeyId);
if (!publicKeyById.isPresent()) {
// key not found
return new Signature(publicKeyId, "gpg", SignatureStatus.NOT_FOUND, null, Collections.emptySet());
}
PublicKey publicKey = publicKeyById.get();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
byte[] headerPrefix = Arrays.copyOfRange(raw, 0, start - GPG_HEADER.length - 1);
baos.write(headerPrefix);
byte[] headerSuffix = Arrays.copyOfRange(raw, end + 1, raw.length);
baos.write(headerSuffix);
} catch (IOException ex) {
// this will never happen, because we are writing into memory
throw new IllegalStateException("failed to write into memory", ex);
}
boolean verified = publicKey.verify(baos.toByteArray(), signature);
return new Signature(
publicKeyId,
"gpg",
verified ? SignatureStatus.VERIFIED : SignatureStatus.INVALID,
publicKey.getOwner().orElse(null),
publicKey.getContacts()
);
}
}

View File

@@ -30,17 +30,22 @@ import com.google.common.base.Function;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevTag;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitUtil; import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Signature;
import sonia.scm.repository.Tag; import sonia.scm.repository.Tag;
import sonia.scm.security.GPG;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Optional;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -49,20 +54,23 @@ import java.util.List;
*/ */
public class GitTagsCommand extends AbstractGitCommand implements TagsCommand { public class GitTagsCommand extends AbstractGitCommand implements TagsCommand {
private final GPG gpg;
/** /**
* Constructs ... * Constructs ...
* *
* @param context * @param context
*/ */
public GitTagsCommand(GitContext context) { public GitTagsCommand(GitContext context, GPG gpp) {
super(context); super(context);
this.gpg = gpp;
} }
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------
@Override @Override
public List<Tag> getTags() throws IOException { public List<Tag> getTags() throws IOException {
List<Tag> tags = null; List<Tag> tags;
RevWalk revWalk = null; RevWalk revWalk = null;
@@ -74,7 +82,7 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand {
List<Ref> tagList = git.tagList().call(); List<Ref> tagList = git.tagList().call();
tags = Lists.transform(tagList, tags = Lists.transform(tagList,
new TransformFuntion(git.getRepository(), revWalk)); new TransformFuntion(git.getRepository(), revWalk, gpg, git));
} catch (GitAPIException ex) { } catch (GitAPIException ex) {
throw new InternalRepositoryException(repository, "could not read tags from repository", ex); throw new InternalRepositoryException(repository, "could not read tags from repository", ex);
} finally { } finally {
@@ -109,9 +117,13 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand {
* @param revWalk * @param revWalk
*/ */
public TransformFuntion(org.eclipse.jgit.lib.Repository repository, public TransformFuntion(org.eclipse.jgit.lib.Repository repository,
RevWalk revWalk) { RevWalk revWalk,
GPG gpg,
Git git) {
this.repository = repository; this.repository = repository;
this.revWalk = revWalk; this.revWalk = revWalk;
this.gpg = gpg;
this.git = git;
} }
//~--- methods ------------------------------------------------------------ //~--- methods ------------------------------------------------------------
@@ -133,6 +145,18 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand {
String name = GitUtil.getTagName(ref); String name = GitUtil.getTagName(ref);
tag = new Tag(name, revObject.getId().name(), GitUtil.getTagTime(revWalk, ref.getObjectId())); tag = new Tag(name, revObject.getId().name(), GitUtil.getTagTime(revWalk, ref.getObjectId()));
try {
RevTag revTag = GitUtil.getTag(repository, revWalk, ref);
final Optional<Signature> tagSignature = GitUtil.getTagSignature(revTag, gpg);
if (tagSignature.isPresent()) {
tag.addSignature(tagSignature.get());
}
} catch (IncorrectObjectTypeException e) {
// Ignore, this must be a lightweight tag
}
} }
} catch (IOException ex) { } catch (IOException ex) {
@@ -153,5 +177,7 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand {
* Field description * Field description
*/ */
private RevWalk revWalk; private RevWalk revWalk;
private final GPG gpg;
private final Git git;
} }
} }

View File

@@ -32,7 +32,6 @@ import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.event.ScmEventBus; import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.Branch; import sonia.scm.repository.Branch;
import sonia.scm.repository.BranchCreatedEvent;
import sonia.scm.repository.PostReceiveRepositoryHookEvent; import sonia.scm.repository.PostReceiveRepositoryHookEvent;
import sonia.scm.repository.PreReceiveRepositoryHookEvent; import sonia.scm.repository.PreReceiveRepositoryHookEvent;
import sonia.scm.repository.api.BranchRequest; import sonia.scm.repository.api.BranchRequest;

View File

@@ -32,6 +32,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.security.GPG;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@@ -45,6 +46,9 @@ class GitRepositoryServiceProviderTest {
@Mock @Mock
private GitContext context; private GitContext context;
@Mock
private GPG gpg;
@Test @Test
void shouldCreatePushCommand() { void shouldCreatePushCommand() {
GitRepositoryServiceProvider provider = createProvider(); GitRepositoryServiceProvider provider = createProvider();
@@ -59,7 +63,7 @@ class GitRepositoryServiceProviderTest {
} }
private GitRepositoryServiceProvider createProvider() { private GitRepositoryServiceProvider createProvider() {
return new GitRepositoryServiceProvider(createParentInjector(), context); return new GitRepositoryServiceProvider(createParentInjector(), context, gpg);
} }
private Injector createParentInjector() { private Injector createParentInjector() {

View File

@@ -0,0 +1,87 @@
/*
* 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 org.eclipse.jgit.lib.GpgSigner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.GitTestHelper;
import sonia.scm.repository.Tag;
import sonia.scm.repository.api.TagDeleteRequest;
import sonia.scm.repository.api.TagCreateRequest;
import sonia.scm.security.GPG;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(MockitoJUnitRunner.class)
public class GitTagCommandTest extends AbstractGitCommandTestBase {
@Mock
private GPG gpg;
@Before
public void setSigner() {
GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner());
}
@Test
public void shouldCreateATag() throws IOException {
createCommand().create(new TagCreateRequest("592d797cd36432e591416e8b2b98154f4f163411", "newtag"));
Optional<Tag> tag = findTag(createContext(), "newtag");
assertThat(tag).isNotEmpty();
}
@Test
public void shouldDeleteATag() throws IOException {
final GitContext context = createContext();
Optional<Tag> tag = findTag(context, "test-tag");
assertThat(tag).isNotEmpty();
createCommand().delete(new TagDeleteRequest("test-tag"));
tag = findTag(context, "test-tag");
assertThat(tag).isEmpty();
}
private GitTagCommand createCommand() {
return new GitTagCommand(createContext(), gpg);
}
private List<Tag> readTags(GitContext context) throws IOException {
return new GitTagsCommand(context, gpg).getTags();
}
private Optional<Tag> findTag(GitContext context, String name) throws IOException {
List<Tag> branches = readTags(context);
return branches.stream().filter(b -> name.equals(b.getName())).findFirst();
}
}

View File

@@ -29,14 +29,22 @@ import com.github.sdorra.shiro.SubjectAware;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.Tag; import sonia.scm.repository.Tag;
import sonia.scm.security.GPG;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import static org.assertj.core.api.Assertions.anyOf;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret") @SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret")
@RunWith(MockitoJUnitRunner.class)
public class GitTagsCommandTest extends AbstractGitCommandTestBase { public class GitTagsCommandTest extends AbstractGitCommandTestBase {
@Rule @Rule
@@ -48,24 +56,41 @@ public class GitTagsCommandTest extends AbstractGitCommandTestBase {
@Rule @Rule
public ShiroRule shiro = new ShiroRule(); public ShiroRule shiro = new ShiroRule();
@Mock
GPG gpg;
@Test @Test
public void shouldGetDatesCorrectly() throws IOException { public void shouldGetDatesCorrectly() throws IOException {
final GitContext gitContext = createContext(); final GitContext gitContext = createContext();
final GitTagsCommand tagsCommand = new GitTagsCommand(gitContext); final GitTagsCommand tagsCommand = new GitTagsCommand(gitContext, gpg);
final List<Tag> tags = tagsCommand.getTags(); final List<Tag> tags = tagsCommand.getTags();
assertThat(tags).hasSize(2); assertThat(tags).hasSize(3);
Tag annotatedTag = tags.get(0); Tag annotatedTag = tags.get(0);
assertThat(annotatedTag.getName()).isEqualTo("1.0.0"); assertThat(annotatedTag.getName()).isEqualTo("1.0.0");
assertThat(annotatedTag.getDate()).contains(1598348105000L); // Annotated - Take tag date assertThat(annotatedTag.getDate()).contains(1598348105000L); // Annotated - Take tag date
assertThat(annotatedTag.getRevision()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec"); assertThat(annotatedTag.getRevision()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec");
Tag lightweightTag = tags.get(1); Tag lightweightTag = tags.get(2);
assertThat(lightweightTag.getName()).isEqualTo("test-tag"); assertThat(lightweightTag.getName()).isEqualTo("test-tag");
assertThat(lightweightTag.getDate()).contains(1339416344000L); // Lightweight - Take commit date assertThat(lightweightTag.getDate()).contains(1339416344000L); // Lightweight - Take commit date
assertThat(lightweightTag.getRevision()).isEqualTo("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1"); assertThat(lightweightTag.getRevision()).isEqualTo("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1");
} }
@Test
public void shouldGetSignatures() throws IOException {
Mockito.when(gpg.findPublicKeyId(ArgumentMatchers.any())).thenReturn("2BA27721F113C005CC16F06BAE63EFBC49F140CF");
final GitContext gitContext = createContext();
final GitTagsCommand tagsCommand = new GitTagsCommand(gitContext, gpg);
final List<Tag> tags = tagsCommand.getTags();
assertThat(tags).hasSize(3);
Tag signedTag = tags.get(1);
assertThat(signedTag.getSignatures()).isNotEmpty();
}
@Override @Override
protected String getZippedRepositoryResource() { protected String getZippedRepositoryResource() {
return "sonia/scm/repository/spi/scm-git-spi-test-tags.zip"; return "sonia/scm/repository/spi/scm-git-spi-test-tags.zip";