This commit is contained in:
Konstantin Schaper
2020-08-03 16:09:37 +02:00
parent 4768e7a1be
commit 08a025ba81
12 changed files with 278 additions and 11 deletions

View File

@@ -164,6 +164,15 @@ public class ModifyCommandBuilder {
return this; return this;
} }
/**
* Set the branch the changes should be made upon.
* @return This builder instance.
*/
public ModifyCommandBuilder disableSignature(String branch) {
request.setBranch(branch);
return this;
}
/** /**
* Set the expected revision of the branch, before the changes are applied. If the branch does not have the * Set the expected revision of the branch, before the changes are applied. If the branch does not have the
* expected revision, a concurrent modification exception will be thrown when the command is executed and no * expected revision, a concurrent modification exception will be thrown when the command is executed and no

View File

@@ -40,9 +40,11 @@ import lombok.ToString;
@NoArgsConstructor @NoArgsConstructor
public class ModificationsCommandRequest implements Resetable { public class ModificationsCommandRequest implements Resetable {
private String revision; private String revision;
private boolean sign = true;
@Override @Override
public void reset() { public void reset() {
revision = null; revision = null;
sign = true;
} }
} }

View File

@@ -49,6 +49,7 @@ public class ModifyCommandRequest implements Resetable, Validateable, CommandWit
private String branch; private String branch;
private String expectedRevision; private String expectedRevision;
private boolean defaultPath; private boolean defaultPath;
private boolean disableSigning;
@Override @Override
public void reset() { public void reset() {
@@ -57,6 +58,7 @@ public class ModifyCommandRequest implements Resetable, Validateable, CommandWit
commitMessage = null; commitMessage = null;
branch = null; branch = null;
defaultPath = false; defaultPath = false;
disableSigning = false;
} }
public void addRequest(PartialRequest request) { public void addRequest(PartialRequest request) {
@@ -112,6 +114,14 @@ public class ModifyCommandRequest implements Resetable, Validateable, CommandWit
this.defaultPath = defaultPath; this.defaultPath = defaultPath;
} }
public boolean isDisableSigning() {
return disableSigning;
}
public void setDisableSigning(boolean disableSigning) {
this.disableSigning = disableSigning;
}
public interface PartialRequest { public interface PartialRequest {
void execute(ModifyCommand.Worker worker) throws IOException; void execute(ModifyCommand.Worker worker) throws IOException;
} }

View File

@@ -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.
*/
package sonia.scm.repository;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.GpgSignature;
import org.eclipse.jgit.lib.GpgSigner;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.transport.CredentialsProvider;
import sonia.scm.security.GPG;
import javax.inject.Inject;
import java.io.UnsupportedEncodingException;
public class ScmGpgSigner extends GpgSigner {
private final GPG gpg;
@Inject
public ScmGpgSigner(GPG gpg) {
this.gpg = gpg;
}
@Override
public void sign(CommitBuilder commitBuilder, String s, PersonIdent personIdent, CredentialsProvider credentialsProvider) throws CanceledException {
try {
final byte[] signature = this.gpg.getPrivateKey().sign(commitBuilder.build());
commitBuilder.setGpgSignature(new GpgSignature(signature));
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}
@Override
public boolean canLocateSigningKey(String s, PersonIdent personIdent, CredentialsProvider credentialsProvider) throws CanceledException {
return true;
}
}

View File

@@ -0,0 +1,54 @@
/*
*
* 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.GpgSigner;
import sonia.scm.plugin.Extension;
import javax.inject.Inject;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
@Extension
public class ScmGpgSignerInitializer implements ServletContextListener {
private final ScmGpgSigner scmGpgSigner;
@Inject
public ScmGpgSignerInitializer(ScmGpgSigner scmGpgSigner) {
this.scmGpgSigner = scmGpgSigner;
}
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
GpgSigner.setDefault(scmGpgSigner);
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
// Do nothing
}
}

View File

@@ -24,20 +24,36 @@
package sonia.scm.security.gpg; package sonia.scm.security.gpg;
import com.sun.tools.javac.util.Pair;
import org.apache.shiro.SecurityUtils;
import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.bcpg.ArmoredInputStream;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPKeyPair;
import org.bouncycastle.openpgp.PGPKeyRingGenerator;
import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.security.GPG; import sonia.scm.security.GPG;
import sonia.scm.security.PrivateKey; import sonia.scm.security.PrivateKey;
import sonia.scm.security.PublicKey; import sonia.scm.security.PublicKey;
import sonia.scm.security.SessionId;
import sonia.scm.user.User;
import javax.inject.Inject; import javax.inject.Inject;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.Collections; import java.util.Collections;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -45,11 +61,13 @@ import java.util.stream.Collectors;
public class DefaultGPG implements GPG { public class DefaultGPG implements GPG {
private static final Logger LOG = LoggerFactory.getLogger(DefaultGPG.class); private static final Logger LOG = LoggerFactory.getLogger(DefaultGPG.class);
private final PublicKeyStore store; private final PublicKeyStore publicKeyStore;
private final PrivateKeyStore privateKeyStore;
@Inject @Inject
public DefaultGPG(PublicKeyStore store) { public DefaultGPG(PublicKeyStore publicKeyStore, PrivateKeyStore privateKeyStore) {
this.store = store; this.publicKeyStore = publicKeyStore;
this.privateKeyStore = privateKeyStore;
} }
@Override @Override
@@ -67,14 +85,14 @@ public class DefaultGPG implements GPG {
@Override @Override
public Optional<PublicKey> findPublicKey(String id) { public Optional<PublicKey> findPublicKey(String id) {
Optional<RawGpgKey> key = store.findById(id); Optional<RawGpgKey> key = publicKeyStore.findById(id);
return key.map(rawGpgKey -> new GpgKey(rawGpgKey.getId(), rawGpgKey.getOwner(), rawGpgKey.getRaw(), rawGpgKey.getContacts())); return key.map(rawGpgKey -> new GpgKey(rawGpgKey.getId(), rawGpgKey.getOwner(), rawGpgKey.getRaw(), rawGpgKey.getContacts()));
} }
@Override @Override
public Iterable<PublicKey> findPublicKeysByUsername(String username) { public Iterable<PublicKey> findPublicKeysByUsername(String username) {
List<RawGpgKey> keys = store.findByUsername(username); List<RawGpgKey> keys = publicKeyStore.findByUsername(username);
if (!keys.isEmpty()) { if (!keys.isEmpty()) {
return keys return keys
@@ -88,6 +106,36 @@ public class DefaultGPG implements GPG {
@Override @Override
public PrivateKey getPrivateKey() { public PrivateKey getPrivateKey() {
throw new UnsupportedOperationException("getPrivateKey is not yet implemented"); final String userId = SecurityUtils.getSubject().getPrincipal().toString();
final Optional<String> privateRawKey = privateKeyStore.getForUserId(userId);
if (!privateRawKey.isPresent()) {
try {
// Generate key pair
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
keyPairGenerator.initialize(2048);
KeyPair pair = keyPairGenerator.generateKeyPair();
String identity = "0xAWESOMExBOB";
PGPKeyPair keyPair = new JcaPGPKeyPair(PGPPublicKey.RSA_GENERAL, pair, new Date());
new PGPKeyRingGenerator().generateSecretKeyRing().;
} catch (PGPException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchProviderException e) {
e.printStackTrace();
}
//
privateKeyStore.setForUserId(user.getId(), privateKeyGpgKeyPair.fst);
publicKeyStore.add(user.getDisplayName(), user.getName(), privateKeyGpgKeyPair.snd.getRaw());
return privateKeyGpgKeyPair.fst;
} else {
// PGPUtil.getDecoderStream();
return privateRawKey.get();
}
} }
} }

View File

@@ -0,0 +1,78 @@
/*
* 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.NoArgsConstructor;
import sonia.scm.security.CipherUtil;
import sonia.scm.security.PrivateKey;
import sonia.scm.store.DataStore;
import sonia.scm.store.DataStoreFactory;
import sonia.scm.xml.XmlInstantAdapter;
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.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.time.Instant;
import java.util.Optional;
@Singleton
class PrivateKeyStore {
private static final String STORE_NAME = "gpg_private_keys";
private final DataStore<RawPrivateKey> store;
@Inject
PrivateKeyStore(DataStoreFactory dataStoreFactory) {
this.store = dataStoreFactory.withType(RawPrivateKey.class).withName(STORE_NAME).build();
}
Optional<String> getForUserId(String userId) {
return store.getOptional(userId).map(rawPrivateKey -> CipherUtil.getInstance().decode(rawPrivateKey.key));
}
void setForUserId(String userId, String rawKey) {
final String encodedRawKey = CipherUtil.getInstance().encode(rawKey);
store.put(userId, new RawPrivateKey(encodedRawKey, Instant.now()));
}
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@AllArgsConstructor
@NoArgsConstructor
private class RawPrivateKey {
private String key;
@XmlJavaTypeAdapter(XmlInstantAdapter.class)
private Instant date;
}
}

View File

@@ -58,7 +58,7 @@ public abstract class PublicKeyMapper {
RawGpgKeyDto createDto(RawGpgKey rawGpgKey) { RawGpgKeyDto createDto(RawGpgKey rawGpgKey) {
Links.Builder linksBuilder = linkingTo(); Links.Builder linksBuilder = linkingTo();
linksBuilder.self(createSelfLink(rawGpgKey)); linksBuilder.self(createSelfLink(rawGpgKey));
if (UserPermissions.changePublicKeys(rawGpgKey.getOwner()).isPermitted()) { if (UserPermissions.changePublicKeys(rawGpgKey.getOwner()).isPermitted() && !rawGpgKey.isReadonly()) {
linksBuilder.single(Link.link("delete", createDeleteLink(rawGpgKey))); linksBuilder.single(Link.link("delete", createDeleteLink(rawGpgKey)));
} }
return new RawGpgKeyDto(linksBuilder.build()); return new RawGpgKeyDto(linksBuilder.build());

View File

@@ -30,12 +30,14 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import sonia.scm.api.v2.resources.ErrorDto; import sonia.scm.api.v2.resources.ErrorDto;
import sonia.scm.security.AllowAnonymousAccess;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.inject.Inject; import javax.inject.Inject;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE; import javax.ws.rs.DELETE;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
@@ -97,6 +99,7 @@ public class PublicKeyResource {
@GET @GET
@Path("{id}") @Path("{id}")
@Produces(MEDIA_TYPE) @Produces(MEDIA_TYPE)
@AllowAnonymousAccess
@Operation( @Operation(
summary = "Get single key for user", summary = "Get single key for user",
description = "Returns a single public key for username by id.", description = "Returns a single public key for username by id.",
@@ -129,7 +132,7 @@ public class PublicKeyResource {
schema = @Schema(implementation = ErrorDto.class) schema = @Schema(implementation = ErrorDto.class)
) )
) )
public Response findById(@PathParam("id") String id) { public Response findByIdJson(@PathParam("id") String id) {
Optional<RawGpgKey> byId = store.findById(id); Optional<RawGpgKey> byId = store.findById(id);
if (byId.isPresent()) { if (byId.isPresent()) {
return Response.ok(mapper.map(byId.get())).build(); return Response.ok(mapper.map(byId.get())).build();

View File

@@ -64,7 +64,7 @@ public class PublicKeyStore {
this.eventBus = eventBus; this.eventBus = eventBus;
} }
public RawGpgKey add(String displayName, String username, String rawKey) { public RawGpgKey add(String displayName, String username, String rawKey, ) {
UserPermissions.changePublicKeys(username).check(); UserPermissions.changePublicKeys(username).check();
if (!rawKey.contains("PUBLIC KEY")) { if (!rawKey.contains("PUBLIC KEY")) {

View File

@@ -50,6 +50,7 @@ public class RawGpgKey {
private String displayName; private String displayName;
private String owner; private String owner;
private String raw; private String raw;
private boolean readonly = false;
private Set<Person> contacts; private Set<Person> contacts;
@XmlJavaTypeAdapter(XmlInstantAdapter.class) @XmlJavaTypeAdapter(XmlInstantAdapter.class)

View File

@@ -97,7 +97,7 @@ class PublicKeyResourceTest {
RawGpgKeyDto dto = new RawGpgKeyDto(); RawGpgKeyDto dto = new RawGpgKeyDto();
when(mapper.map(key)).thenReturn(dto); when(mapper.map(key)).thenReturn(dto);
Response response = resource.findById("42"); Response response = resource.findByIdJson("42");
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getEntity()).isSameAs(dto); assertThat(response.getEntity()).isSameAs(dto);
} }
@@ -106,7 +106,7 @@ class PublicKeyResourceTest {
void shouldReturn404IfIdDoesNotExists() { void shouldReturn404IfIdDoesNotExists() {
when(store.findById("42")).thenReturn(Optional.empty()); when(store.findById("42")).thenReturn(Optional.empty());
Response response = resource.findById("42"); Response response = resource.findByIdJson("42");
assertThat(response.getStatus()).isEqualTo(404); assertThat(response.getStatus()).isEqualTo(404);
} }