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;
}
/**
* 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
* 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
public class ModificationsCommandRequest implements Resetable {
private String revision;
private boolean sign = true;
@Override
public void reset() {
revision = null;
sign = true;
}
}

View File

@@ -49,6 +49,7 @@ public class ModifyCommandRequest implements Resetable, Validateable, CommandWit
private String branch;
private String expectedRevision;
private boolean defaultPath;
private boolean disableSigning;
@Override
public void reset() {
@@ -57,6 +58,7 @@ public class ModifyCommandRequest implements Resetable, Validateable, CommandWit
commitMessage = null;
branch = null;
defaultPath = false;
disableSigning = false;
}
public void addRequest(PartialRequest request) {
@@ -112,6 +114,14 @@ public class ModifyCommandRequest implements Resetable, Validateable, CommandWit
this.defaultPath = defaultPath;
}
public boolean isDisableSigning() {
return disableSigning;
}
public void setDisableSigning(boolean disableSigning) {
this.disableSigning = disableSigning;
}
public interface PartialRequest {
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;
import com.sun.tools.javac.util.Pair;
import org.apache.shiro.SecurityUtils;
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.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.security.GPG;
import sonia.scm.security.PrivateKey;
import sonia.scm.security.PublicKey;
import sonia.scm.security.SessionId;
import sonia.scm.user.User;
import javax.inject.Inject;
import java.io.ByteArrayInputStream;
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.Date;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -45,11 +61,13 @@ import java.util.stream.Collectors;
public class DefaultGPG implements GPG {
private static final Logger LOG = LoggerFactory.getLogger(DefaultGPG.class);
private final PublicKeyStore store;
private final PublicKeyStore publicKeyStore;
private final PrivateKeyStore privateKeyStore;
@Inject
public DefaultGPG(PublicKeyStore store) {
this.store = store;
public DefaultGPG(PublicKeyStore publicKeyStore, PrivateKeyStore privateKeyStore) {
this.publicKeyStore = publicKeyStore;
this.privateKeyStore = privateKeyStore;
}
@Override
@@ -67,14 +85,14 @@ public class DefaultGPG implements GPG {
@Override
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()));
}
@Override
public Iterable<PublicKey> findPublicKeysByUsername(String username) {
List<RawGpgKey> keys = store.findByUsername(username);
List<RawGpgKey> keys = publicKeyStore.findByUsername(username);
if (!keys.isEmpty()) {
return keys
@@ -88,6 +106,36 @@ public class DefaultGPG implements GPG {
@Override
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) {
Links.Builder linksBuilder = linkingTo();
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)));
}
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.responses.ApiResponse;
import sonia.scm.api.v2.resources.ErrorDto;
import sonia.scm.security.AllowAnonymousAccess;
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.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@@ -97,6 +99,7 @@ public class PublicKeyResource {
@GET
@Path("{id}")
@Produces(MEDIA_TYPE)
@AllowAnonymousAccess
@Operation(
summary = "Get single key for user",
description = "Returns a single public key for username by id.",
@@ -129,7 +132,7 @@ public class PublicKeyResource {
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);
if (byId.isPresent()) {
return Response.ok(mapper.map(byId.get())).build();

View File

@@ -64,7 +64,7 @@ public class PublicKeyStore {
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();
if (!rawKey.contains("PUBLIC KEY")) {

View File

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

View File

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